Simplified AndroidListener abstraction Registration retries are no longer automatic but exponential backoff is used when the application manually retries. The invalidation client no longer sends optimistic registration success messages so we can use those messages to reset backoff delays. git-svn-id: http://google-cache-invalidation-api.googlecode.com/svn/trunk@246 1cc9d426-c294-39be-ba72-c0199ca0f247 
diff --git a/src/example-app-build/AndroidManifest.xml b/src/example-app-build/AndroidManifest.xml index 490c4c5..90a24c7 100644 --- a/src/example-app-build/AndroidManifest.xml +++ b/src/example-app-build/AndroidManifest.xml 
@@ -20,7 +20,7 @@    <application>  <!-- Configure the listener class for the application --> - <meta-data android:name="ipc.invalidation.ticl.listener_class" + <meta-data android:name="ipc.invalidation.ticl.listener_service_class"  android:value="com.google.ipc.invalidation.examples.android2.ExampleListener"/>    <!-- Example activity --> @@ -31,8 +31,12 @@  </intent-filter>  </activity>   - <!-- Service handling authorization token requests --> - <service android:exported="false" android:name=".ExampleService"> + <!-- Receiver for scheduler alarms. Must be exported for the AlarmManager to call it. --> + <receiver android:exported="true" + android:name="com.google.ipc.invalidation.external.client.contrib.AndroidListener$AlarmReceiver"/> + + <!-- Ticl listener. --> + <service android:exported="false" android:name=".ExampleListener">  <intent-filter>  <action android:name="com.google.ipc.invalidation.AUTH_TOKEN_REQUEST"/>  </intent-filter> @@ -43,10 +47,6 @@  <service android:exported="false"  android:name="com.google.ipc.invalidation.ticl.android2.TiclService"/>   - <!-- Ticl listener. --> - <service android:exported="false" - android:name="com.google.ipc.invalidation.ticl.android2.AndroidInvalidationListenerStub"/> -  <!-- Ticl sender. -->  <service android:exported="false"  android:name="com.google.ipc.invalidation.ticl.android2.channel.AndroidMessageSenderService"/> 
diff --git a/src/example-app-build/proguard.cfg b/src/example-app-build/proguard.cfg index 78c21ee..9533f06 100644 --- a/src/example-app-build/proguard.cfg +++ b/src/example-app-build/proguard.cfg 
@@ -49,7 +49,6 @@  # All changes below are additions to the Android SDK defaults, generally for the purposes of  # suppressing spurious or inconsequential warnings.  # --keep public class com.google.ipc.invalidation.examples.android2.ExampleListener    # Suppress duplicate warning for system classes; Blaze is passing android.jar  # to proguard multiple times. 
diff --git a/src/java/com/google/ipc/invalidation/common/CommonProtos2.java b/src/java/com/google/ipc/invalidation/common/CommonProtos2.java index 769f1b9..6b3b3ad 100644 --- a/src/java/com/google/ipc/invalidation/common/CommonProtos2.java +++ b/src/java/com/google/ipc/invalidation/common/CommonProtos2.java 
@@ -123,30 +123,23 @@  .build();  }   - public static InvalidationP newInvalidationP(ObjectIdP oid, long version) { - return newInvalidationP(oid, version, null, null, null); - } -  public static InvalidationP newInvalidationP(ObjectIdP oid, long version,  TrickleState trickleState) { - return newInvalidationP(oid, version, null, null, trickleState); + return newInvalidationP(oid, version, trickleState, null, null);  }    public static InvalidationP newInvalidationP(ObjectIdP oid, long version, - ByteString payload, Long bridgeArrivalTimeMs) { - return newInvalidationP(oid, version, payload, bridgeArrivalTimeMs, null); + TrickleState trickleState, ByteString payload) { + return newInvalidationP(oid, version, trickleState, payload, null);  }    public static InvalidationP newInvalidationP(ObjectIdP oid, long version, - ByteString payload, Long bridgeArrivalTimeMs, - TrickleState trickleState) { + TrickleState trickleState, ByteString payload, Long bridgeArrivalTimeMs) {  InvalidationP.Builder builder = InvalidationP.newBuilder()  .setObjectId(oid)  .setIsKnownVersion(true) - .setVersion(version); - if (trickleState != null) { - builder.setIsTrickleRestart(trickleState == TrickleState.RESTART); - } + .setVersion(version) + .setIsTrickleRestart(trickleState == TrickleState.RESTART);  if (payload != null) {  builder.setPayload(payload);  } @@ -156,14 +149,40 @@  return builder.build();  }   - public static InvalidationP newInvalidationPForUnknownVersion(ObjectIdP oid, long version) { + public static InvalidationP newInvalidationPForUnknownVersion(ObjectIdP oid, + long sequenceNumber) {  return InvalidationP.newBuilder()  .setObjectId(oid)  .setIsKnownVersion(false) - .setVersion(version) + .setIsTrickleRestart(true) + .setVersion(sequenceNumber)  .build();  }   + /** + * Returns an invalidation that is identical to {@code invalidation} but with the + * {@code is_trickle_restart} flag set to true. If the input {@invalidation} is already restarted, + * it is returned directly. Otherwise, a new invalidation is created. + */ + public static InvalidationP toRestartedInvalidation(InvalidationP invalidation) { + if (invalidation.hasIsTrickleRestart() && invalidation.getIsTrickleRestart()) { + return invalidation; + } + return invalidation.toBuilder().setIsTrickleRestart(true).build(); + } + + /** + * Returns an invalidation that is identical to {@code invalidation} but with the + * {@code is_trickle_restart} flag set to false. If the input {@invalidation} is already + * a continuous invalidation, it is returned directly. Otherwise, a new invalidation is created. + */ + public static InvalidationP toContinuousInvalidation(InvalidationP invalidation) { + if (invalidation.hasIsTrickleRestart() && !invalidation.getIsTrickleRestart()) { + return invalidation; + } + return invalidation.toBuilder().setIsTrickleRestart(false).build(); + } +  public static RegistrationP newRegistrationP(ObjectIdP oid, boolean isReg) {  RegistrationP registration = RegistrationP.newBuilder()  .setObjectId(oid) @@ -172,6 +191,14 @@  return registration;  }   + public static RegistrationP newRegistrationPForRegistration(ObjectIdP oid) { + return newRegistrationP(oid, true); + } + + public static RegistrationP newRegistrationPForUnregistration(ObjectIdP oid) { + return newRegistrationP(oid, false); + } +  public static StatusP newSuccessStatus() {  return StatusP.newBuilder().setCode(StatusP.Code.SUCCESS).build();  } @@ -369,16 +396,8 @@  AndroidChannel.EndpointId.Builder endpointBuilder = AndroidChannel.EndpointId.newBuilder()  .setC2DmRegistrationId(registrationId)  .setClientKey(clientKey) - .setPackageName(packageName); - - // The protocol version field was set in the INITIAL channel implementation but subsequent - // versions only set the channel version. - if (channelVersion == null) { - // TODO: Remove once unversioned clients are no longer supported - endpointBuilder.setProtocolVersion(CommonInvalidationConstants2.PROTOCOL_VERSION); - } else { - endpointBuilder.setChannelVersion(channelVersion); - } + .setPackageName(packageName) + .setChannelVersion(channelVersion);  return newNetworkEndpointId(NetworkAddress.ANDROID, endpointBuilder.build().toByteString());  }   
diff --git a/src/java/com/google/ipc/invalidation/common/TiclMessageValidator2.java b/src/java/com/google/ipc/invalidation/common/TiclMessageValidator2.java index 3e9c395..7b6e2b5 100644 --- a/src/java/com/google/ipc/invalidation/common/TiclMessageValidator2.java +++ b/src/java/com/google/ipc/invalidation/common/TiclMessageValidator2.java 
@@ -138,9 +138,10 @@  // Note that a missing value for is_trickle_restart is treated like a true value,  // becomes it comes from a downlevel client that uses invalidation semantics.  boolean isTrickleRestart = !invalidation.hasIsTrickleRestart() || - !invalidation.getIsTrickleRestart(); + invalidation.getIsTrickleRestart();  if (isUnknownVersion && !isTrickleRestart) { - logger.info("is_trickle_restart must be true or missing if is_known_version is false: %s", + logger.info( + "if is_known_version is false, is_trickle_restart must be true or missing: %s",  invalidation);  return false;  } 
diff --git a/src/java/com/google/ipc/invalidation/examples/android2/ExampleListener.java b/src/java/com/google/ipc/invalidation/examples/android2/ExampleListener.java index 62a5ffd..4f1ef81 100644 --- a/src/java/com/google/ipc/invalidation/examples/android2/ExampleListener.java +++ b/src/java/com/google/ipc/invalidation/examples/android2/ExampleListener.java 
@@ -15,49 +15,71 @@  */  package com.google.ipc.invalidation.examples.android2;   -import com.google.ipc.invalidation.external.client.InvalidationClient; -import com.google.ipc.invalidation.external.client.InvalidationListener; -import com.google.ipc.invalidation.external.client.types.AckHandle; +import com.google.common.base.Preconditions; +import com.google.ipc.invalidation.external.client.InvalidationListener.RegistrationState; +import com.google.ipc.invalidation.external.client.contrib.AndroidListener;  import com.google.ipc.invalidation.external.client.types.ErrorInfo;  import com.google.ipc.invalidation.external.client.types.Invalidation;  import com.google.ipc.invalidation.external.client.types.ObjectId; -import com.google.protos.ipc.invalidation.Types.ObjectSource;   +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AccountManagerFuture; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.app.PendingIntent; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.os.Bundle; +import android.util.Base64;  import android.util.Log;   +import java.io.IOException; +import java.util.ArrayList;  import java.util.HashSet; +import java.util.List;  import java.util.Set;   +  /** - * Implements the service that handles events for this application. The android library - * encapsulates all the intent handling and just raises events using the general-purpose - * {@link InvalidationListener} interface. This example listener registers an interest in a small - * number of objects and calls {@link MainActivity} with any relevant status changes. - * <p> - * Because InvalidationListener does not derive from one of the usual proguard protected types, - * e.g. {@link android.app.IntentService}, you must manually suppress trimming of the class by - * adding a line - * <p> - * <code>-keep public class com.google.ipc.invalidation.examples.android2.ExampleListener</code> - * <p> - * to your proguard configuration file. + * Implements the service that handles invalidation client events for this application. This + * listener registers an interest in a small number of objects and calls {@link MainActivity} with + * any relevant status changes.  *  */ -public final class ExampleListener implements InvalidationListener { +public final class ExampleListener extends AndroidListener { + + /** The account type value for Google accounts */ + private static final String GOOGLE_ACCOUNT_TYPE = "com.google"; + + /** + * This is the authentication token type that's used for invalidation client communication to the + * server. For real applications, it would generally match the authorization type used by the + * application. + */ + private static final String AUTH_TYPE = "android"; + + /** Name used for shared preferences. */ + private static final String PREFERENCES_NAME = "example_listener"; + + /** Key used for listener state in shared preferences. */ + private static final String STATE_KEY = "example_listener_state";    /** Object source for objects the client is tracking. */ - private static final int DEMO_SOURCE = ObjectSource.Type.DEMO_VALUE; + private static final int DEMO_SOURCE = 4;    /** The tag used for logging in the listener. */  private static final String TAG = "TEA2:ExampleListener";    /** Number of objects we're interested in tracking. */ - private static final int NUM_INTERESTING_OBJECTS = 10; + private static final int NUM_INTERESTING_OBJECTS = 4;    /** Ids for objects we want to track. */  private final Set<ObjectId> interestingObjects;    public ExampleListener() { + super();  // We're interested in objects with ids Obj0, Obj1, ...  interestingObjects = new HashSet<ObjectId>();  for (int i = 1; i <= NUM_INTERESTING_OBJECTS; i++) { @@ -67,7 +89,7 @@  }    @Override - public void informError(InvalidationClient client, ErrorInfo errorInfo) { + public void informError(ErrorInfo errorInfo) {  Log.e(TAG, "informError: " + errorInfo);    /*********************************************************************************************** @@ -78,91 +100,34 @@  }    @Override - public void informRegistrationFailure(final InvalidationClient client, final ObjectId objectId, - boolean isTransient, String errorMessage) { - Log.e(TAG, "informRegistrationFailure: " + objectId + " " + errorMessage); - - /*********************************************************************************************** - * YOUR CODE HERE - * - * In case of transient registration failures, retries should use exponential back-off to avoid - * excessive load. Handling of permanent failures is application-specific. - **********************************************************************************************/ - - MainActivity.State.setRegistrationStatus(objectId, "Error: " + errorMessage); - - if (!isTransient) { - // There's nothing we can do with a permanent error! - return; - } - - // If the error is transient, send another registration or unregistration request depending - // on whether we're interested in tracking the object or not. - if (interestingObjects.contains(objectId)) { - client.register(objectId); - } else { - client.unregister(objectId); - } + public void reissueRegistrations(byte[] clientID) { + Log.i(TAG, "reissueRegistrations()"); + register(clientID, interestingObjects);  }    @Override - public void informRegistrationStatus(InvalidationClient client, ObjectId objectId, - RegistrationState regState) { - Log.i(TAG, "informRegistrationStatus: " + objectId + " " + regState); - - /*********************************************************************************************** - * YOUR CODE HERE - * - * will inform client applications of registration status for objects after calls to - * InvalidationClient.(un)register. It is the responsibility of the client application to verify - * that the registration status is consistent with its own expectations and to make additional - * (un)register calls as needed. - **********************************************************************************************/ - - MainActivity.State.setRegistrationStatus(objectId, "Status: " + regState); - - // If the registration status is what we want, ignore. Otherwise, send another registration - // request. - if (interestingObjects.contains(objectId)) { - if (RegistrationState.UNREGISTERED.equals(regState)) { - // is informing us that an object we are interested in tracking is unregistered. Send - // a register request. - client.register(objectId); - } - } else { - if (RegistrationState.REGISTERED.equals(regState)) { - // is informing us that an object we are not interested in tracking is registered. - // Send an unregister request. - client.unregister(objectId); - } - } - } - - @Override - public void invalidate(InvalidationClient client, Invalidation invalidation, - AckHandle ackHandle) { + public void invalidate(Invalidation invalidation, byte[] ackHandle) {  Log.i(TAG, "invalidate: " + invalidation);    // Do real work here based upon the invalidation  MainActivity.State.setVersion(invalidation.getObjectId(),  "Version from invalidate: " + invalidation.getVersion());   - client.acknowledge(ackHandle); + acknowledge(ackHandle);  }    @Override - public void invalidateUnknownVersion(InvalidationClient client, ObjectId objectId, - AckHandle ackHandle) { + public void invalidateUnknownVersion(ObjectId objectId, byte[] ackHandle) {  Log.i(TAG, "invalidateUnknownVersion: " + objectId);    // Do real work here based upon the invalidation.  MainActivity.State.setVersion(objectId, "Version from backend: " + getBackendVersion(objectId));   - client.acknowledge(ackHandle); + acknowledge(ackHandle);  }    @Override - public void invalidateAll(InvalidationClient client, AckHandle ackHandle) { + public void invalidateAll(byte[] ackHandle) {  Log.i(TAG, "invalidateAll");    // Do real work here based upon the invalidation. @@ -170,30 +135,140 @@  MainActivity.State.setVersion(objectId,  "Version from backend: " + getBackendVersion(objectId));  } - client.acknowledge(ackHandle); + + acknowledge(ackHandle); + } + + + @Override + public byte[] readState() { + Log.i(TAG, "readState"); + SharedPreferences sharedPreferences = getSharedPreferences(); + String data = sharedPreferences.getString(STATE_KEY, null); + return (data != null) ? Base64.decode(data, Base64.DEFAULT) : null;  }    @Override - public void ready(InvalidationClient client) { - Log.i(TAG, "ready"); + public void writeState(byte[] data) { + Log.i(TAG, "writeState"); + Editor editor = getSharedPreferences().edit(); + editor.putString(STATE_KEY, Base64.encodeToString(data, Base64.DEFAULT)); + editor.commit();  }    @Override - public void reissueRegistrations(InvalidationClient client, byte[] prefix, int prefixLength) { - // Reissue registrations for all invalidations. This method will be called for a newly started - // client. - Log.i(TAG, "reissueRegistrations: " + prefix + " " + prefixLength); - for (ObjectId objectId : interestingObjects) { - client.register(objectId); + public void requestAuthToken(PendingIntent pendingIntent, + String invalidAuthToken) { + Log.i(TAG, "requestAuthToken"); + + // In response to requestAuthToken, we need to get an auth token and inform the invalidation + // client of the result through a call to setAuthToken. In this example, we block until a + // result is available. It is also possible to invoke setAuthToken in a callback or when + // handling an intent. + AccountManager accountManager = AccountManager.get(getApplicationContext()); + + // Invalidate the old token if necessary. + if (invalidAuthToken != null) { + accountManager.invalidateAuthToken(GOOGLE_ACCOUNT_TYPE, invalidAuthToken);  } + + // Choose an (arbitrary in this example) account for which to retrieve an authentication token. + Account account = getAccount(accountManager); + + try { + // There are three possible outcomes of the call to getAuthToken: + // + // 1. Authentication failure (null result). + // 2. The user needs to sign in or give permission for the account. In such cases, the result + // includes an intent that can be started to retrieve credentials from the user. + // 3. The response includes the auth token, in which case we can inform the invalidation + // client. + // + // In the first case, we simply log and return. The response to such errors is application- + // specific. For instance, the application may prompt the user to choose another account. + // + // In the second case, we start an intent to ask for user credentials so that they are + // available to the application if there is a future request. An application should listen for + // the LOGIN_ACCOUNTS_CHANGED_ACTION broadcast intent to trigger a response to the + // invalidation client after the user has responded. Otherwise, it may take several minutes + // for the invalidation client to start. + // + // In the third case, success!, we pass the authorization token and type to the invalidation + // client using the setAuthToken method. + AccountManagerFuture<Bundle> future = accountManager.getAuthToken(account, AUTH_TYPE, false, + null, null); + Bundle result = future.getResult(); + if (result == null) { + // If the result is null, it means that authentication was not possible. + Log.w(TAG, "Auth token - getAuthToken returned null"); + return; + } + if (result.containsKey(AccountManager.KEY_INTENT)) { + Log.i(TAG, "Starting intent to get auth credentials"); + + // Need to start intent to get credentials. + Intent intent = result.getParcelable(AccountManager.KEY_INTENT); + int flags = intent.getFlags(); + flags |= Intent.FLAG_ACTIVITY_NEW_TASK; + intent.setFlags(flags); + getApplicationContext().startActivity(intent); + return; + } + String authToken = result.getString(AccountManager.KEY_AUTHTOKEN); + setAuthToken(getApplicationContext(), pendingIntent, authToken, AUTH_TYPE); + } catch (OperationCanceledException e) { + Log.w(TAG, "Auth token - operation cancelled", e); + } catch (AuthenticatorException e) { + Log.w(TAG, "Auth token - authenticator exception", e); + } catch (IOException e) { + Log.w(TAG, "Auth token - IO exception", e); + } + } + + /** Returns any Google account enabled on the device. */ + private static Account getAccount(AccountManager accountManager) { + Preconditions.checkNotNull(accountManager); + for (Account acct : accountManager.getAccounts()) { + if (GOOGLE_ACCOUNT_TYPE.equals(acct.type)) { + return acct; + } + } + throw new RuntimeException("No google account enabled."); + } + + @Override + public void informRegistrationFailure(byte[] clientId, ObjectId objectId, boolean isTransient, + String errorMessage) { + Log.e(TAG, "Registration failure!"); + if (isTransient) { + // Retry immediately on transient failures. The base AndroidListener will handle exponential + // backoff if there are repeated failures. + List<ObjectId> objectIds = new ArrayList<ObjectId>(); + objectIds.add(objectId); + if (interestingObjects.contains(objectId)) { + register(clientId, objectIds); + } else { + unregister(clientId, objectIds); + } + } + } + + @Override + public void informRegistrationStatus(byte[] clientId, ObjectId objectId, + RegistrationState regState) { + Log.i(TAG, "informRegistrationStatus"); + } + + private SharedPreferences getSharedPreferences() { + return getApplicationContext().getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE);  }    private long getBackendVersion(ObjectId objectId) {  /***********************************************************************************************  * YOUR CODE HERE  * - * has no information about the given object. Connect with the application backend to - * determine its current state. The implementation should be non-blocking. + * Invalidation client has no information about the given object. Connect with the application + * backend to determine its current state. The implementation should be non-blocking.  **********************************************************************************************/    // Normally, we would connect to a real application backend. For this example, we return a fixed 
diff --git a/src/java/com/google/ipc/invalidation/examples/android2/ExampleService.java b/src/java/com/google/ipc/invalidation/examples/android2/ExampleService.java deleted file mode 100644 index 47478bf..0000000 --- a/src/java/com/google/ipc/invalidation/examples/android2/ExampleService.java +++ /dev/null 
@@ -1,129 +0,0 @@ -/* - * Copyright 2011 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.ipc.invalidation.examples.android2; - -import com.google.common.base.Preconditions; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.accounts.AccountManagerCallback; -import android.accounts.AccountManagerFuture; -import android.app.IntentService; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; - -/** - * Example of an {@link Intent} service that responds to authorization token requests from the - * client service. - * - */ -public final class ExampleService extends IntentService { - - /** The tag used for logging. */ - private static final String TAG = "TEA2:ExampleService"; - - /** - * Action requesting that an authorization token to send a message be provided. This is the action - * used in the intent to the application. - */ - private static final String ACTION_REQUEST_AUTH_TOKEN = - "com.google.ipc.invalidation.AUTH_TOKEN_REQUEST"; - - /** Extra in an authorization token request response providing the pending intent. */ - private static final String EXTRA_PENDING_INTENT = - "com.google.ipc.invalidation.AUTH_TOKEN_PENDING_INTENT"; - - /** Extra in the intent from the application that provides the authorization token string. */ - private static final String EXTRA_AUTH_TOKEN = "com.google.ipc.invalidation.AUTH_TOKEN"; - - /** Extra in the intent from the application that provides the authorization token type. */ - private static final String EXTRA_AUTH_TOKEN_TYPE = "com.google.ipc.invalidation.AUTH_TOKEN_TYPE"; - - /** - * Extra in an authorization token request message indicating that the token provided as the value - * was invalid when last used. This may be set on the intent to the application. - */ -  - private static final String EXTRA_INVALIDATE_AUTH_TOKEN = - "com.google.ipc.invalidaton.AUTH_TOKEN_INVALIDATE"; - - /** The account type value for Google accounts */ - private static final String GOOGLE_ACCOUNT_TYPE = "com.google"; - - /** - * This is the authentication token type that's used for communication to the server. - * For real applications, it would generally match the authorization type used by the application. - */ - private static final String AUTH_TYPE = "android"; - - public ExampleService() { - super("ExampleService"); - setIntentRedelivery(true); - } - - @Override - protected void onHandleIntent(Intent intent) { - if ((null != intent) && ACTION_REQUEST_AUTH_TOKEN.equals(intent.getAction())) { - handleAuthTokenRequest(intent); - } else { - Log.e(TAG, "Unhandled intent: " + intent); - } - } - - /** Handles a request for an authorization token to send a message. */ - private void handleAuthTokenRequest(Intent intent) { - final Context context = getApplicationContext(); - AccountManager accountManager = AccountManager.get(context); - if (intent.hasExtra(EXTRA_INVALIDATE_AUTH_TOKEN)) { - // Tell the account manager there is a stale authorization token. - String authToken = intent.getStringExtra(EXTRA_INVALIDATE_AUTH_TOKEN); - Log.i(TAG, "Invalidating authorization token " + authToken); - accountManager.invalidateAuthToken(GOOGLE_ACCOUNT_TYPE, authToken); - } - Account account = getAccount(accountManager); - Log.i(TAG, "Retrieving authorization token for account " + account.name); - final PendingIntent pendingIntent = intent.getParcelableExtra(EXTRA_PENDING_INTENT); - accountManager.getAuthToken(account, AUTH_TYPE, true, new AccountManagerCallback<Bundle>() { - @Override - public void run(AccountManagerFuture<Bundle> future) { - try { - Bundle result = future.getResult(); - String authToken = result.getString(AccountManager.KEY_AUTHTOKEN); - Intent responseIntent = new Intent() - .putExtra(EXTRA_AUTH_TOKEN, authToken) - .putExtra(EXTRA_AUTH_TOKEN_TYPE, AUTH_TYPE); - pendingIntent.send(context, 0, responseIntent); - } catch (Exception e) { - Log.e(TAG, "Unable to get authorization token", e); - } - } - }, null); - } - - /** Returns the first Google account enabled on the device. */ - private static Account getAccount(AccountManager accountManager) { - Preconditions.checkNotNull(accountManager); - for (Account acct : accountManager.getAccounts()) { - if (GOOGLE_ACCOUNT_TYPE.equals(acct.type)) { - return acct; - } - } - throw new RuntimeException("No google account enabled."); - } -} 
diff --git a/src/java/com/google/ipc/invalidation/examples/android2/MainActivity.java b/src/java/com/google/ipc/invalidation/examples/android2/MainActivity.java index 27f4b6f..1719f83 100644 --- a/src/java/com/google/ipc/invalidation/examples/android2/MainActivity.java +++ b/src/java/com/google/ipc/invalidation/examples/android2/MainActivity.java 
@@ -17,12 +17,12 @@  package com.google.ipc.invalidation.examples.android2;    import com.google.android.gcm.GCMRegistrar; -import com.google.ipc.invalidation.external.client.android2.AndroidClientFactory; +import com.google.ipc.invalidation.external.client.contrib.AndroidListener;  import com.google.ipc.invalidation.external.client.types.ObjectId; -import com.google.protos.ipc.invalidation.Types.ClientType.Type;    import android.app.Activity;  import android.content.Context; +import android.content.Intent;  import android.os.Bundle;  import android.util.Log;  import android.widget.TextView; @@ -42,7 +42,7 @@  --port=8888 --channelUri="talkgadget.google.com" --use_lcs=false  * </code>  * - * <p>Just publish invalidations with ids similar to 'Obj0', 'Obj1', ... 'Obj9' + * <p>Just publish invalidations with ids similar to 'Obj1', 'Obj2', ... 'Obj3'  *  */  public final class MainActivity extends Activity { @@ -51,7 +51,7 @@  private static final String TAG = "TEA2:MainActivity";    /** Ticl client configuration. */ - private static final Type clientType = Type.DEMO; + private static final int CLIENT_TYPE = 4; // Demo client ID.  private static final byte[] CLIENT_NAME = "TEA2:eetrofoot".getBytes();    /** Sender ID associated with 's Android Push Messaging quota. */ @@ -63,18 +63,10 @@  * essentials in this example.  */  public static final class State { - private static final Map<ObjectId, String> registrationStatus = new HashMap<ObjectId, String>();  private static final Map<ObjectId, String> lastInformedVersion =  new HashMap<ObjectId, String>();  private static volatile MainActivity currentActivity;   - public static void setRegistrationStatus(ObjectId objectId, String status) { - synchronized (registrationStatus) { - registrationStatus.put(objectId, status); - } - refreshData(); - } -  public static void setVersion(ObjectId objectId, String version) {  synchronized (lastInformedVersion) {  lastInformedVersion.put(objectId, version); @@ -89,13 +81,16 @@  /** Called when the activity is first created. */  @Override  public void onCreate(Bundle savedInstanceState) { + Log.i(TAG, "Creating main activity");  super.onCreate(savedInstanceState);    initializeGcm();   - // createClient will create and start a client. When the client is available, or if there is an - // existing client, InvalidationListener.ready() is called. - AndroidClientFactory.createClient(getApplicationContext(), clientType, CLIENT_NAME); + // Create and start a notification client. When the client is available, or if there is an + // existing client, AndroidListener.reissueRegistrations() is called. + Context context = getApplicationContext(); + Intent startIntent = AndroidListener.createStartIntent(context, CLIENT_TYPE, CLIENT_NAME); + context.startService(startIntent);    // Setup UI.  info = new TextView(this); @@ -126,13 +121,6 @@  final MainActivity activity = State.currentActivity;  if (null != activity) {  final StringBuilder builder = new StringBuilder(); - builder.append("Registration status\n---------------\n"); - synchronized (State.registrationStatus) { - for (Entry<ObjectId, String> entry : State.registrationStatus.entrySet()) { - builder.append(entry.getKey().toString()).append(" -> ").append(entry.getValue()) - .append("\n"); - } - }  builder.append("\nLast informed versions status\n---------------\n");  synchronized (State.lastInformedVersion) {  for (Entry<ObjectId, String> entry : State.lastInformedVersion.entrySet()) { 
diff --git a/src/java/com/google/ipc/invalidation/external/client/contrib/AndroidListener.java b/src/java/com/google/ipc/invalidation/external/client/contrib/AndroidListener.java new file mode 100644 index 0000000..ec0907e --- /dev/null +++ b/src/java/com/google/ipc/invalidation/external/client/contrib/AndroidListener.java 
@@ -0,0 +1,612 @@ +/* + * Copyright 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.ipc.invalidation.external.client.contrib; + +import com.google.common.base.Preconditions; +import com.google.ipc.invalidation.external.client.InvalidationClient; +import com.google.ipc.invalidation.external.client.InvalidationListener; +import com.google.ipc.invalidation.external.client.InvalidationListener.RegistrationState; +import com.google.ipc.invalidation.external.client.SystemResources.Logger; +import com.google.ipc.invalidation.external.client.android.service.AndroidLogger; +import com.google.ipc.invalidation.external.client.types.AckHandle; +import com.google.ipc.invalidation.external.client.types.ErrorInfo; +import com.google.ipc.invalidation.external.client.types.Invalidation; +import com.google.ipc.invalidation.external.client.types.ObjectId; +import com.google.ipc.invalidation.ticl.ProtoConverter; +import com.google.ipc.invalidation.ticl.android2.AndroidClock; +import com.google.ipc.invalidation.ticl.android2.AndroidInvalidationListenerIntentMapper; +import com.google.ipc.invalidation.ticl.android2.ProtocolIntents; +import com.google.ipc.invalidation.ticl.android2.channel.AndroidChannelConstants.AuthTokenConstants; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protos.ipc.invalidation.AndroidListenerProtocol; +import com.google.protos.ipc.invalidation.AndroidListenerProtocol.RegistrationCommand; +import com.google.protos.ipc.invalidation.AndroidListenerProtocol.StartCommand; +import com.google.protos.ipc.invalidation.ClientProtocol.ClientConfigP; +import com.google.protos.ipc.invalidation.ClientProtocol.ObjectIdP; + +import android.app.IntentService; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import java.util.concurrent.TimeUnit; + + +/** + * Simplified listener contract for Android clients. Takes care of exponential back-off when + * register or unregister are called for an object after a failure has occurred. Also suppresses + * redundant register requests. + * + * <p>If the subclass is {@code namespace.ExampleListener}, you will need to add the following lines + * to the <code><application></code> element in your Android manifest file: + * + * <p><code> + * <!-- Configure the listener class for the application --> + * <meta-data android:name="ipc.invalidation.ticl.listener_service_class" + * android:value="namespace.ExampleListener"/> + * + * <!-- Ticl listener. --> + * <service android:exported="false" android:name="namespace.ExampleListener"> + * <intent-filter> + * <action android:name="com.google.ipc.invalidation.AUTH_TOKEN_REQUEST"/> + * </intent-filter> + * </service> + * + * <!-- Receiver for scheduler alarms. Must be exported for the AlarmManager to call it. --> + * <receiver android:exported="true" android:name= + * "com.google.ipc.invalidation.external.client.contrib.AndroidListener$AlarmReceiver"/> + * </code> + * + * <p>A sample implementation of an {@link AndroidListener} is shown below: + * + * <p><code> + * class ExampleListener extends AndroidListener { + * @Override + * public void reissueRegistrations(byte[] clientId) { + * List<ObjectId> desiredRegistrations = ...; + * register(clientId, desiredRegistrations); + * } + * + * @Override + * public void invalidate(Invalidation invalidation, final byte[] ackHandle) { + * // Track the most recent version of the object (application-specific) and then acknowledge + * // the invalidation. + * ... + * acknowledge(ackHandle); + * } + * + * @Override + * public void informRegistrationFailure(byte[] clientId, ObjectId objectId, + * boolean isTransient, String errorMessage) { + * // Try again if there is a transient failure and we still care whether the object is + * // registered or not. + * if (isTransient) { + * boolean shouldRetry = ...; + * if (shouldRetry) { + * boolean shouldBeRegistered = ...; + * if (shouldBeRegistered) { + * register(clientId, ImmutableList.of(objectId)); + * } else { + * unregister(clientId, ImmutableList.of(objectId)); + * } + * } + * } + * } + * + * ... + * } + * </code> + * + */ +public abstract class AndroidListener extends IntentService { + + /** External alarm receiver that allows the listener to respond to alarm intents. */ + public static final class AlarmReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + Preconditions.checkNotNull(context); + Preconditions.checkNotNull(intent); + if (intent.hasExtra(AndroidListenerIntents.EXTRA_REGISTRATION)) { + AndroidListenerIntents.issueAndroidListenerIntent(context, intent); + } + } + } + + /** The logger. */ + private static final Logger logger = AndroidLogger.forPrefix(""); + + /** Initial retry delay for exponential backoff (1 minute). */ + private static final int INITIAL_MAX_DELAY_MS = (int) TimeUnit.SECONDS.toMillis(60); + + /** Maximum delay factor for exponential backoff (6 hours). */ + private static final int MAX_DELAY_FACTOR = 6 * 60; + + /** + * Invalidation listener implementation. We implement the interface on a private field rather + * than directly to avoid leaking methods that should not be directly called by the client + * application. The listener must be called only on intent service thread. + */ + private final InvalidationListener invalidationListener = new InvalidationListener() { + @Override + public final void ready(final InvalidationClient client) { + // We rely on reissueRegistrations being called by the TICL service after ready(). + logger.info("ready() upcall received."); + } + + @Override + public final void reissueRegistrations(final InvalidationClient client, byte[] prefix, + int prefixLength) { + AndroidListener.this.reissueRegistrations(state.getClientId().toByteArray()); + } + + @Override + public final void informRegistrationStatus(final InvalidationClient client, + final ObjectId objectId, final RegistrationState regState) { + state.informRegistrationSuccess(objectId); + AndroidListener.this.informRegistrationStatus(state.getClientId().toByteArray(), + objectId, regState); + } + + @Override + public final void informRegistrationFailure(final InvalidationClient client, + final ObjectId objectId, final boolean isTransient, final String errorMessage) { + state.informRegistrationFailure(objectId, isTransient); + AndroidListener.this.informRegistrationFailure(state.getClientId().toByteArray(), objectId, + isTransient, errorMessage); + } + + @Override + public void invalidate(InvalidationClient client, Invalidation invalidation, + AckHandle ackHandle) { + AndroidListener.this.invalidate(invalidation, ackHandle.getHandleData()); + } + + @Override + public void invalidateUnknownVersion(InvalidationClient client, ObjectId objectId, + AckHandle ackHandle) { + AndroidListener.this.invalidateUnknownVersion(objectId, ackHandle.getHandleData()); + } + + @Override + public void invalidateAll(InvalidationClient client, AckHandle ackHandle) { + AndroidListener.this.invalidateAll(ackHandle.getHandleData()); + } + + @Override + public void informError(InvalidationClient client, ErrorInfo errorInfo) { + AndroidListener.this.informError(errorInfo); + } + }; + + /** + * The internal state of the listener. Lazy initialization, triggered by {@link #onHandleIntent}. + */ + private AndroidListenerState state; + + /** The clock to use when scheduling retry call-backs. */ + private final AndroidClock clock = new AndroidClock.SystemClock(); + + /** + * The mapper used to route intents to the invalidation listener. Lazy initialization triggered + * by {@link #onCreate}. + */ + private AndroidInvalidationListenerIntentMapper intentMapper; + + /** Initializes {@link AndroidListener}. */ + protected AndroidListener() { + super(""); + + // If the process dies before an intent is handled, setIntentRedelivery(true) ensures that the + // last intent is redelivered. This optimization is not necessary for correctness: on restart, + // all registrations will be reissued and unacked invalidations will be resent anyways. + setIntentRedelivery(true); + } + + /** See specs for {@link InvalidationClient#start}. */ + public static Intent createStartIntent(Context context, int clientType, byte[] clientName) { + Preconditions.checkNotNull(context); + Preconditions.checkNotNull(clientName); + + return AndroidListenerIntents.createStartIntent(context, clientType, clientName); + } + + /** See specs for {@link InvalidationClient#stop}. */ + public static Intent createStopIntent(Context context) { + Preconditions.checkNotNull(context); + + return AndroidListenerIntents.createStopIntent(context); + } + + /** + * See specs for {@link InvalidationClient#register}. + * + * @param context the context + * @param clientId identifier for the client service for which we are registering + * @param objectIds the object ids being registered + */ + public static Intent createRegisterIntent(Context context, byte[] clientId, + Iterable<ObjectId> objectIds) { + Preconditions.checkNotNull(context); + Preconditions.checkNotNull(clientId); + Preconditions.checkNotNull(objectIds); + + final boolean isRegister = true; + return AndroidListenerIntents.createRegistrationIntent(context, clientId, objectIds, + isRegister); + } + + /** + * See specs for {@link InvalidationClient#register}. + * + * @param clientId identifier for the client service for which we are registering + * @param objectIds the object ids being registered + */ + public void register(byte[] clientId, Iterable<ObjectId> objectIds) { + Preconditions.checkNotNull(clientId); + Preconditions.checkNotNull(objectIds); + + Context context = getApplicationContext(); + context.startService(createRegisterIntent(context, clientId, objectIds)); + } + + /** + * See specs for {@link InvalidationClient#unregister}. + * + * @param context the context + * @param clientId identifier for the client service for which we are unregistering + * @param objectIds the object ids being unregistered + */ + public static Intent createUnregisterIntent(Context context, byte[] clientId, + Iterable<ObjectId> objectIds) { + Preconditions.checkNotNull(context); + Preconditions.checkNotNull(clientId); + Preconditions.checkNotNull(objectIds); + + final boolean isRegister = false; + return AndroidListenerIntents.createRegistrationIntent(context, clientId, objectIds, + isRegister); + } + + /** + * Sets the authorization token and type used by the invalidation client. Call in response to + * {@link #requestAuthToken} calls. + * + * @param pendingIntent pending intent passed to {@link #requestAuthToken} + * @param authToken authorization token + * @param authType authorization token typo + */ + public static void setAuthToken(Context context, PendingIntent pendingIntent, String authToken, + String authType) { + Preconditions.checkNotNull(pendingIntent); + Preconditions.checkNotNull(authToken); + Preconditions.checkNotNull(authType); + + AndroidListenerIntents.issueAuthTokenResponse(context, pendingIntent, authToken, authType); + } + + /** + * See specs for {@link InvalidationClient#unregister}. + * + * @param clientId identifier for the client service for which we are registering + * @param objectIds the object ids being unregistered + */ + public void unregister(byte[] clientId, Iterable<ObjectId> objectIds) { + Preconditions.checkNotNull(clientId); + Preconditions.checkNotNull(objectIds); + + Context context = getApplicationContext(); + context.startService(createUnregisterIntent(context, clientId, objectIds)); + } + + /** See specs for {@link InvalidationClient#acknowledge}. */ + public static Intent createAcknowledgeIntent(Context context, byte[] ackHandle) { + Preconditions.checkNotNull(context); + Preconditions.checkNotNull(ackHandle); + + return AndroidListenerIntents.createAckIntent(context, ackHandle); + } + + /** See specs for {@link InvalidationClient#acknowledge}. */ + public void acknowledge(byte[] ackHandle) { + Preconditions.checkNotNull(ackHandle); + + Context context = getApplicationContext(); + context.startService(createAcknowledgeIntent(context, ackHandle)); + } + + /** + * See specs for {@link InvalidationListener#reissueRegistrations}. + * + * @param clientId the client identifier that must be passed to {@link #createRegisterIntent} + * and {@link #createUnregisterIntent} + */ + public abstract void reissueRegistrations(byte[] clientId); + + /** + * See specs for {@link InvalidationListener#informError}. + */ + public abstract void informError(ErrorInfo errorInfo); + + /** + * See specs for {@link InvalidationListener#invalidate}. + * + * @param invalidation the invalidation + * @param ackHandle event acknowledgment handle + */ + public abstract void invalidate(Invalidation invalidation, byte[] ackHandle); + + /** + * See specs for {@link InvalidationListener#invalidateUnknownVersion}. + * + * @param objectId identifier for the object with unknown version + * @param ackHandle event acknowledgment handle + */ + public abstract void invalidateUnknownVersion(ObjectId objectId, byte[] ackHandle); + + /** + * See specs for {@link InvalidationListener#invalidateAll}. + * + * @param ackHandle event acknowledgment handle + */ + public abstract void invalidateAll(byte[] ackHandle); + + /** + * Read listener state. + * + * @return serialized state or {@code null} if it is not available + */ + public abstract byte[] readState(); + + /** Write listener state to some location. */ + public abstract void writeState(byte[] data); + + /** + * See specs for {@link InvalidationListener#informRegistrationFailure}. + */ + public abstract void informRegistrationFailure(byte[] clientId, ObjectId objectId, + boolean isTransient, String errorMessage); + + /** + * See specs for (@link InvalidationListener#informRegistrationStatus}. + */ + public abstract void informRegistrationStatus(byte[] clientId, ObjectId objectId, + RegistrationState regState); + + /** + * Called when an authorization token is needed. Respond by calling {@link #setAuthToken}. + * + * @param pendingIntent pending intent that must be used in {@link #setAuthToken} response. + * @param invalidAuthToken the existing invalid token or null if none exists. Implementation + * should invalidate the token. + */ + public abstract void requestAuthToken(PendingIntent pendingIntent, + String invalidAuthToken); + + @Override + public void onCreate() { + super.onCreate(); + + // Initialize the intent mapper (now that context is available). + intentMapper = new AndroidInvalidationListenerIntentMapper(invalidationListener, this); + } + + @Override + protected void onHandleIntent(Intent intent) { + if (intent == null) { + return; + } + + // We lazily initialize state in calls to onHandleIntent rather than initializing in onCreate + // because onCreate runs on the UI thread and initializeState performs I/O. + if (state == null) { + initializeState(); + } + + // Handle any intents specific to the AndroidListener. For other intents, defer to the + // intentMapper, which handles listener upcalls corresponding to the InvalidationListener + // methods. + if (!tryHandleAuthTokenRequestIntent(intent) && + !tryHandleRegistrationIntent(intent) && + !tryHandleStartIntent(intent) && + !tryHandleStopIntent(intent) && + !tryHandleAckIntent(intent)) { + intentMapper.handleIntent(intent); + } + + // Always check to see if we need to persist state changes after handling an intent. + if (state.getIsDirty()) { + writeState(state.marshal().toByteArray()); + state.resetIsDirty(); + } + } + + /** Returns invalidation client that can be used to trigger intents against the TICL service. */ + private InvalidationClient getClient() { + return intentMapper.client; + } + + /** + * Initializes listener state either from persistent proto (if available) or from scratch. + */ + private void initializeState() { + AndroidListenerProtocol.AndroidListenerState proto = getPersistentState(); + if (proto != null) { + state = new AndroidListenerState(getInitialMaxDelayMs(), getMaxDelayFactor(), proto); + } else { + state = new AndroidListenerState(getInitialMaxDelayMs(), getMaxDelayFactor()); + } + } + + /** Gets initial maximum retry delay for exponential backoff. Can be overridden for tests. */ +  + int getInitialMaxDelayMs() { + return INITIAL_MAX_DELAY_MS; + } + + /** + * Gets maximum delay factor for exponential backoff (relative to {@link #getInitialMaxDelayMs}). + */ +  + int getMaxDelayFactor() { + return MAX_DELAY_FACTOR; + } + + /** + * Reads and parses persistent state for the listener. Returns {@code null} if the state does not + * exist or is invalid. + */ + private AndroidListenerProtocol.AndroidListenerState getPersistentState() { + // Defer to application code to read the blob containing the state proto. + byte[] stateData = readState(); + try { + if (null != stateData) { + AndroidListenerProtocol.AndroidListenerState state = + AndroidListenerProtocol.AndroidListenerState.parseFrom(stateData); + if (!AndroidListenerProtos.isValidAndroidListenerState(state)) { + logger.warning("Invalid listener state."); + return null; + } + return state; + } + } catch (InvalidProtocolBufferException exception) { + logger.warning("Failed to parse listener state: %s", exception); + } + return null; + } + + /** + * Tries to handle a request for an authorization token. Returns {@code true} iff the intent is + * an auth token request. + */ + private boolean tryHandleAuthTokenRequestIntent(Intent intent) { + if (!AndroidListenerIntents.isAuthTokenRequest(intent)) { + return false; + } + Context context = getApplicationContext(); + + // Check for invalid auth token. Subclass may have to invalidate it if it exists in the call + // to getNewAuthToken. + String invalidAuthToken = intent.getStringExtra( + AuthTokenConstants.EXTRA_INVALIDATE_AUTH_TOKEN); + // Intent also includes a pending intent that we can use to pass back our response. + PendingIntent pendingIntent = intent.getParcelableExtra( + AuthTokenConstants.EXTRA_PENDING_INTENT); + if (pendingIntent == null) { + logger.warning("Authorization request without pending intent extra."); + } else { + // Delegate to client application to figure out what the new token should be and the auth + // type. + requestAuthToken(pendingIntent, invalidAuthToken); + } + return true; + } + + /** Tries to handle a stop intent. Returns {@code true} iff the intent is a stop intent. */ + private boolean tryHandleStopIntent(Intent intent) { + if (!AndroidListenerIntents.isStopIntent(intent)) { + return false; + } + getClient().stop(); + return true; + } + + /** + * Tries to handle a registration intent. Returns {@code true} iff the intent is a registration + * intent. + */ + private boolean tryHandleRegistrationIntent(Intent intent) { + RegistrationCommand command = AndroidListenerIntents.findRegistrationCommand(intent); + if ((command == null) || !AndroidListenerProtos.isValidRegistrationCommand(command)) { + return false; + } + // Make sure the registration is intended for this client. If not, we ignore it (suggests + // there is a new client now). + if (!command.getClientId().equals(state.getClientId())) { + logger.warning("Ignoring registration request for old client. Old ID = {0}, New ID = {1}", + command.getClientId(), state.getClientId()); + return true; + } + boolean isRegister = command.getIsRegister(); + for (ObjectIdP objectIdP : command.getObjectIdList()) { + ObjectId objectId = ProtoConverter.convertFromObjectIdProto(objectIdP); + // We may need to delay the registration command (if it is not already delayed). + int delayMs = 0; + if (!command.getIsDelayed()) { + delayMs = state.getNextDelay(objectId); + } + if (delayMs == 0) { + issueRegistration(objectId, isRegister); + } else { + AndroidListenerIntents.issueDelayedRegistrationIntent(getApplicationContext(), clock, + state.getClientId(), objectId, isRegister, delayMs, state.getNextRequestCode()); + } + } + return true; + } + + /** + * Called when the client application requests a new registration. If a redundant register request + * is made -- i.e. when the application attempts to register an object that is already in the + * {@code AndroidListenerState#desiredRegistrations} collection -- the method returns immediately. + * Unregister requests are never ignored since we can't reliably determine whether an unregister + * request is redundant: our policy on failures of any kind is to remove the registration from + * the {@code AndroidListenerState#desiredRegistrations} collection. + */ + private void issueRegistration(ObjectId objectId, boolean isRegister) { + if (isRegister) { + if (state.addDesiredRegistration(objectId)) { + // Don't bother if we think it's already registered. Note that we remove the object from the + // collection when there is a failure. + getClient().register(objectId); + } + } else { + getClient().unregister(objectId); + } + } + + /** Tries to handle a start intent. Returns {@code true} iff the intent is a start intent. */ + private boolean tryHandleStartIntent(Intent intent) { + StartCommand command = AndroidListenerIntents.findStartCommand(intent); + if ((command == null) || !AndroidListenerProtos.isValidStartCommand(command)) { + return false; + } + // Reset the state so that we make no assumptions about desired registrations and can ignore + // messages directed at the wrong instance. + state = new AndroidListenerState(getInitialMaxDelayMs(), getMaxDelayFactor()); + boolean skipStartForTest = false; + Intent startIntent = ProtocolIntents.InternalDowncalls.newCreateClientIntent( + command.getClientType(), command.getClientName().toByteArray(), + ClientConfigP.getDefaultInstance(), skipStartForTest); + AndroidListenerIntents.issueTiclIntent(getApplicationContext(), startIntent); + return true; + } + + /** Tries to handle an ack intent. Returns {@code true} iff the intent is an ack intent. */ + private boolean tryHandleAckIntent(Intent intent) { + byte[] data = AndroidListenerIntents.findAckHandle(intent); + if (data == null) { + return false; + } + getClient().acknowledge(AckHandle.newInstance(data)); + return true; + } + + /** Returns the current state of the listener, for tests. */ + AndroidListenerState getStateForTest() { + return state; + } +} 
diff --git a/src/java/com/google/ipc/invalidation/external/client/contrib/AndroidListenerIntents.java b/src/java/com/google/ipc/invalidation/external/client/contrib/AndroidListenerIntents.java new file mode 100644 index 0000000..bd66ae5 --- /dev/null +++ b/src/java/com/google/ipc/invalidation/external/client/contrib/AndroidListenerIntents.java 
@@ -0,0 +1,220 @@ +/* + * Copyright 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.ipc.invalidation.external.client.contrib; + +import com.google.ipc.invalidation.external.client.SystemResources.Logger; +import com.google.ipc.invalidation.external.client.android.service.AndroidLogger; +import com.google.ipc.invalidation.external.client.contrib.AndroidListener.AlarmReceiver; +import com.google.ipc.invalidation.external.client.types.ObjectId; +import com.google.ipc.invalidation.ticl.android2.AndroidClock; +import com.google.ipc.invalidation.ticl.android2.AndroidTiclManifest; +import com.google.ipc.invalidation.ticl.android2.channel.AndroidChannelConstants.AuthTokenConstants; +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protos.ipc.invalidation.AndroidListenerProtocol.RegistrationCommand; +import com.google.protos.ipc.invalidation.AndroidListenerProtocol.StartCommand; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.app.PendingIntent.CanceledException; +import android.content.Context; +import android.content.Intent; + + +/** + * Static helper class supporting construction and decoding of intents issued and handled by the + * {@link AndroidListener}. + * + */ +class AndroidListenerIntents { + + /** The logger. */ + private static final Logger logger = AndroidLogger.forPrefix(""); + + /** Key of Intent byte[] holding a {@link RegistrationCommand} protocol buffer. */ + static final String EXTRA_REGISTRATION = + "com.google.ipc.invalidation.android_listener.REGISTRATION"; + + /** Key of Intent byte[] holding a {@link StartCommand} protocol buffer. */ + static final String EXTRA_START = + "com.google.ipc.invalidation.android_listener.START"; + + /** Key of Intent extra indicating that the client should stop. */ + static final String EXTRA_STOP = + "com.google.ipc.invalidation.android_listener.STOP"; + + /** Key of Intent extra holding a byte[] that is ack handle data. */ + static final String EXTRA_ACK = + "com.google.ipc.invalidation.android_listener.ACK"; + + /** + * Issues the given {@code intent} to the TICL service class registered in the {@code context}. + */ + static void issueTiclIntent(Context context, Intent intent) { + context.startService(intent.setClassName(context, + new AndroidTiclManifest(context).getTiclServiceClass())); + } + + /** + * Issues the given {@code intent} to the {@link AndroidListener} class registered in the + * {@code context}. + */ + static void issueAndroidListenerIntent(Context context, Intent intent) { + context.startService(setAndroidListenerClass(context, intent)); + } + + /** + * Returns the ack handle from the given intent if it has the appropriate extra. Otherwise, + * returns {@code null}. + */ + static byte[] findAckHandle(Intent intent) { + return intent.getByteArrayExtra(EXTRA_ACK); + } + + /** + * Returns {@link RegistrationCommand} extra from the given intent or null if no valid + * registration command exists. + */ + static RegistrationCommand findRegistrationCommand(Intent intent) { + // Check that the extra exists. + byte[] data = intent.getByteArrayExtra(EXTRA_REGISTRATION); + if (null == data) { + return null; + } + + // Attempt to parse the extra. + try { + return RegistrationCommand.parseFrom(data); + } catch (InvalidProtocolBufferException exception) { + logger.warning("Received invalid proto: %s", exception); + return null; + } + } + + /** + * Returns {@link StartCommand} extra from the given intent or null if no valid start command + * exists. + */ + static StartCommand findStartCommand(Intent intent) { + // Check that the extra exists. + byte[] data = intent.getByteArrayExtra(EXTRA_START); + if (null == data) { + return null; + } + + // Attempt to parse the extra. + try { + return StartCommand.parseFrom(data); + } catch (InvalidProtocolBufferException exception) { + logger.warning("Received invalid proto: %s", exception); + return null; + } + } + + /** Returns {@code true} if the intent has the 'stop' extra. */ + static boolean isStopIntent(Intent intent) { + return intent.hasExtra(EXTRA_STOP); + } + + /** Issues a registration retry with delay. */ + static void issueDelayedRegistrationIntent(Context context, AndroidClock clock, + ByteString clientId, ObjectId objectId, boolean isRegister, int delayMs, int requestCode) { + RegistrationCommand command = isRegister ? + AndroidListenerProtos.newDelayedRegisterCommand(clientId, objectId) : + AndroidListenerProtos.newDelayedUnregisterCommand(clientId, objectId); + Intent intent = new Intent() + .putExtra(EXTRA_REGISTRATION, command.toByteArray()) + .setClass(context, AlarmReceiver.class); + + // Create a pending intent that will cause the AlarmManager to fire the above intent. + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, requestCode, intent, + PendingIntent.FLAG_ONE_SHOT); + + // Schedule the pending intent after the appropriate delay. + AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + long executeMs = clock.nowMs() + delayMs; + alarmManager.set(AlarmManager.RTC, executeMs, pendingIntent); + } + + /** Creates a 'start-client' intent. */ + static Intent createStartIntent(Context context, int clientType, byte[] clientName) { + Intent intent = new Intent(); + // Create proto for the start command. + StartCommand command = AndroidListenerProtos.newStartCommand(clientType, + ByteString.copyFrom(clientName)); + intent.putExtra(EXTRA_START, command.toByteArray()); + return setAndroidListenerClass(context, intent); + } + + /** Creates a 'stop-client' intent. */ + static Intent createStopIntent(Context context) { + // Stop command just has the extra (its content doesn't matter). + Intent intent = new Intent(); + intent.putExtra(EXTRA_STOP, true); + return setAndroidListenerClass(context, intent); + } + + /** Create an ack intent. */ + static Intent createAckIntent(Context context, byte[] ackHandle) { + // Ack intent has an extra containing the ack handle data. + Intent intent = new Intent(); + intent.putExtra(EXTRA_ACK, ackHandle); + return setAndroidListenerClass(context, intent); + } + + /** Constructs an intent with {@link RegistrationCommand} proto. */ + static Intent createRegistrationIntent(Context context, byte[] clientId, + Iterable<ObjectId> objectIds, boolean isRegister) { + // Registration intent has an extra containing the RegistrationCommand proto. + Intent intent = new Intent(); + RegistrationCommand command = isRegister + ? AndroidListenerProtos.newRegisterCommand(ByteString.copyFrom(clientId), objectIds) + : AndroidListenerProtos.newUnregisterCommand(ByteString.copyFrom(clientId), objectIds); + intent.putExtra(EXTRA_REGISTRATION, command.toByteArray()); + return setAndroidListenerClass(context, intent); + } + + /** Sets the appropriate class for {@link AndroidListener} service intents. */ + static Intent setAndroidListenerClass(Context context, Intent intent) { + String simpleListenerClass = new AndroidTiclManifest(context).getListenerServiceClass(); + return intent.setClassName(context, simpleListenerClass); + } + + /** Returns {@code true} iff the given intent is an authorization token request. */ + static boolean isAuthTokenRequest(Intent intent) { + return AuthTokenConstants.ACTION_REQUEST_AUTH_TOKEN.equals(intent.getAction()); + } + + /** + * Given an authorization token request intent and authorization information ({@code authToken} + * and {@code authType}) issues a response. + */ + static void issueAuthTokenResponse(Context context, PendingIntent pendingIntent, String authToken, + String authType) { + Intent responseIntent = new Intent() + .putExtra(AuthTokenConstants.EXTRA_AUTH_TOKEN, authToken) + .putExtra(AuthTokenConstants.EXTRA_AUTH_TOKEN_TYPE, authType); + try { + pendingIntent.send(context, 0, responseIntent); + } catch (CanceledException exception) { + logger.warning("Canceled auth request: %s", exception); + } + } + + // Prevent instantiation. + private AndroidListenerIntents() { + } +} 
diff --git a/src/java/com/google/ipc/invalidation/external/client/contrib/AndroidListenerProtos.java b/src/java/com/google/ipc/invalidation/external/client/contrib/AndroidListenerProtos.java new file mode 100644 index 0000000..ca8bd93 --- /dev/null +++ b/src/java/com/google/ipc/invalidation/external/client/contrib/AndroidListenerProtos.java 
@@ -0,0 +1,137 @@ +/* + * Copyright 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.ipc.invalidation.external.client.contrib; + +import com.google.common.base.Preconditions; +import com.google.ipc.invalidation.external.client.types.ObjectId; +import com.google.ipc.invalidation.ticl.ProtoConverter; +import com.google.ipc.invalidation.ticl.TiclExponentialBackoffDelayGenerator; +import com.google.protobuf.ByteString; +import com.google.protos.ipc.invalidation.AndroidListenerProtocol.AndroidListenerState; +import com.google.protos.ipc.invalidation.AndroidListenerProtocol.AndroidListenerState.RetryRegistrationState; +import com.google.protos.ipc.invalidation.AndroidListenerProtocol.RegistrationCommand; +import com.google.protos.ipc.invalidation.AndroidListenerProtocol.StartCommand; + +import java.util.Map; +import java.util.Map.Entry; + +/** + * Static helper class supporting construction of valid {code AndroidListenerProtocol} messages. + * + */ +class AndroidListenerProtos { + + /** Creates a register command for the given objects and client. */ + static RegistrationCommand newRegisterCommand(ByteString clientId, Iterable<ObjectId> objectIds) { + final boolean isRegister = true; + return newRegistrationCommand(clientId, objectIds, isRegister); + } + + /** Creates an unregister command for the given objects and client. */ + static RegistrationCommand newUnregisterCommand(ByteString clientId, + Iterable<ObjectId> objectIds) { + final boolean isRegister = false; + return newRegistrationCommand(clientId, objectIds, isRegister); + } + + /** Creates a retry register command for the given object and client. */ + static RegistrationCommand newDelayedRegisterCommand(ByteString clientId, ObjectId objectId) { + final boolean isRegister = true; + return newDelayedRegistrationCommand(clientId, objectId, isRegister); + } + + /** Creates a retry unregister command for the given object and client. */ + static RegistrationCommand newDelayedUnregisterCommand(ByteString clientId, ObjectId objectId) { + final boolean isRegister = false; + return newDelayedRegistrationCommand(clientId, objectId, isRegister); + } + + /** Creates proto for {@link AndroidListener} state. */ + static AndroidListenerState newAndroidListenerState(ByteString clientId, int requestCodeSeqNum, + Map<ObjectId, TiclExponentialBackoffDelayGenerator> delayGenerators, + Iterable<ObjectId> desiredRegistrations) { + AndroidListenerState.Builder builder = AndroidListenerState.newBuilder() + .setClientId(clientId) + .setRequestCodeSeqNum(requestCodeSeqNum); + for (ObjectId objectId : desiredRegistrations) { + builder.addRegistration(ProtoConverter.convertToObjectIdProto(objectId)); + } + for (Entry<ObjectId, TiclExponentialBackoffDelayGenerator> entry : delayGenerators.entrySet()) { + builder.addRetryRegistrationState( + newRetryRegistrationState(entry.getKey(), entry.getValue())); + } + return builder.build(); + } + + /** Creates proto for retry registration state. */ + static RetryRegistrationState newRetryRegistrationState(ObjectId objectId, + TiclExponentialBackoffDelayGenerator delayGenerator) { + return RetryRegistrationState.newBuilder() + .setObjectId(ProtoConverter.convertToObjectIdProto(objectId)) + .setExponentialBackoffState(delayGenerator.marshal()) + .build(); + } + + /** Returns {@code true} iff the given proto is valid. */ + static boolean isValidAndroidListenerState(AndroidListenerState state) { + return state.hasClientId() && state.hasRequestCodeSeqNum(); + } + + /** Returns {@code true} iff the given proto is valid. */ + static boolean isValidRegistrationCommand(RegistrationCommand command) { + return command.hasIsRegister() && command.hasClientId() && command.hasIsDelayed(); + } + + /** Returns {@code true} iff the given proto is valid. */ + static boolean isValidStartCommand(StartCommand command) { + return command.hasClientType() && command.hasClientName(); + } + + /** Creates start command proto. */ + static StartCommand newStartCommand(int clientType, ByteString clientName) { + return StartCommand.newBuilder() + .setClientType(clientType) + .setClientName(clientName) + .build(); + } + + private static RegistrationCommand newRegistrationCommand(ByteString clientId, + Iterable<ObjectId> objectIds, boolean isRegister) { + RegistrationCommand.Builder builder = RegistrationCommand.newBuilder() + .setIsRegister(isRegister) + .setClientId(clientId) + .setIsDelayed(false); + for (ObjectId objectId : objectIds) { + Preconditions.checkNotNull(objectId); + builder.addObjectId(ProtoConverter.convertToObjectIdProto(objectId)); + } + return builder.build(); + } + + private static RegistrationCommand newDelayedRegistrationCommand(ByteString clientId, + ObjectId objectId, boolean isRegister) { + return RegistrationCommand.newBuilder() + .setIsRegister(isRegister) + .addObjectId(ProtoConverter.convertToObjectIdProto(objectId)) + .setClientId(clientId) + .setIsDelayed(true) + .build(); + } + + // Prevent instantiation. + private AndroidListenerProtos() { + } +} 
diff --git a/src/java/com/google/ipc/invalidation/external/client/contrib/AndroidListenerState.java b/src/java/com/google/ipc/invalidation/external/client/contrib/AndroidListenerState.java new file mode 100644 index 0000000..026b5ef --- /dev/null +++ b/src/java/com/google/ipc/invalidation/external/client/contrib/AndroidListenerState.java 
@@ -0,0 +1,300 @@ +/* + * Copyright 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.ipc.invalidation.external.client.contrib; + +import com.google.ipc.invalidation.external.client.types.ObjectId; +import com.google.ipc.invalidation.ticl.ProtoConverter; +import com.google.ipc.invalidation.ticl.TiclExponentialBackoffDelayGenerator; +import com.google.ipc.invalidation.util.Bytes; +import com.google.ipc.invalidation.util.Marshallable; +import com.google.ipc.invalidation.util.TypedUtil; +import com.google.protobuf.ByteString; +import com.google.protos.ipc.invalidation.AndroidListenerProtocol; +import com.google.protos.ipc.invalidation.AndroidListenerProtocol.AndroidListenerState.RetryRegistrationState; +import com.google.protos.ipc.invalidation.ClientProtocol.ObjectIdP; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Random; +import java.util.Set; +import java.util.UUID; + + +/** + * Encapsulates state to simplify persistence and tracking of changes. Internally maintains an + * {@link #isDirty} bit. Call {@link #resetIsDirty} to indicate that changes have been persisted. + * + * <p>Notes on the {@link #desiredRegistrations} (DR) and {@link #delayGenerators} (DG) collections: + * When the client application registers for an object, it is immediately added to DR. Similarly, + * an object is removed from DR when the application unregisters. If a registration failure is + * reported, the object is removed from DR if it exists and a delay generator is added to DG if one + * does not already exist. (In the face of a failure, we assume that the registration is not desired + * by the application unless/until the application retries.) When there is a successful + * registration, the corresponding DG entry is removed. There are two independent collections rather + * than one since we may be applying exponential backoff for an object when it is not in DR, and we + * may have no reason to delay operations against an object in DR as well. + * + * <p>By removing objects from the {@link #desiredRegistrations} collection on failures, we are + * essentially assuming that the client application doesn't care about the registration until we're + * told otherwise -- by a subsequent call to register or unregister. + * + */ +final class AndroidListenerState + implements Marshallable<AndroidListenerProtocol.AndroidListenerState> { + + /** + * Exponential backoff delay generators used to determine delay before registration retries. + * There is a delay generator for every failing object. + */ + private final Map<ObjectId, TiclExponentialBackoffDelayGenerator> delayGenerators = + new HashMap<ObjectId, TiclExponentialBackoffDelayGenerator>(); + + /** The set of registrations for which the client wants to be registered. */ + private final Set<ObjectId> desiredRegistrations; + + /** Random generator used for all delay generators. */ + private final Random random = new Random(); + + /** Initial maximum retry delay for exponential backoff. */ + private final int initialMaxDelayMs; + + /** Maximum delay factor for exponential backoff (relative to {@link #initialMaxDelayMs}). */ + private final int maxDelayFactor; + + /** Sequence number for alarm manager request codes. */ + private int requestCodeSeqNum; + + /** + * Dirty flag. {@code true} whenever changes are made, reset to false when {@link #resetIsDirty} + * is called. State initialized from a proto is assumed to be initially clean. + */ + private boolean isDirty; + + /** + * The identifier for the current client. The ID is randomly generated and is used to ensure that + * messages are not handled by the wrong client instance. + */ + private final ByteString clientId; + + /** Initializes state for a new client. */ + AndroidListenerState(int initialMaxDelayMs, int maxDelayFactor) { + desiredRegistrations = new HashSet<ObjectId>(); + clientId = createGloballyUniqueClientId(); + // Assigning a client ID dirties the state because calling the constructor twice produces + // different results. + isDirty = true; + requestCodeSeqNum = 0; + this.initialMaxDelayMs = initialMaxDelayMs; + this.maxDelayFactor = maxDelayFactor; + } + + /** Initializes state from proto. */ + AndroidListenerState(int initialMaxDelayMs, int maxDelayFactor, + AndroidListenerProtocol.AndroidListenerState state) { + desiredRegistrations = new HashSet<ObjectId>(); + for (ObjectIdP objectIdProto : state.getRegistrationList()) { + desiredRegistrations.add(ProtoConverter.convertFromObjectIdProto(objectIdProto)); + } + for (RetryRegistrationState retryState : state.getRetryRegistrationStateList()) { + ObjectId objectId = ProtoConverter.convertFromObjectIdProto(retryState.getObjectId()); + delayGenerators.put(objectId, new TiclExponentialBackoffDelayGenerator(random, + initialMaxDelayMs, maxDelayFactor, retryState.getExponentialBackoffState())); + } + clientId = state.getClientId(); + requestCodeSeqNum = state.getRequestCodeSeqNum(); + isDirty = false; + this.initialMaxDelayMs = initialMaxDelayMs; + this.maxDelayFactor = maxDelayFactor; + } + + /** Increments and returns sequence number for alarm manager request codes. */ + int getNextRequestCode() { + isDirty = true; + return ++requestCodeSeqNum; + } + + /** + * See specs for {@link TiclExponentialBackoffDelayGenerator#getNextDelay}. Gets next delay for + * the given {@code objectId}. If a delay generator does not yet exist for the object, one is + * created. + */ + int getNextDelay(ObjectId objectId) { + TiclExponentialBackoffDelayGenerator delayGenerator = + delayGenerators.get(objectId); + if (delayGenerator == null) { + delayGenerator = new TiclExponentialBackoffDelayGenerator(random, initialMaxDelayMs, + maxDelayFactor); + delayGenerators.put(objectId, delayGenerator); + } + // Requesting a delay from a delay generator modifies its internal state. + isDirty = true; + return delayGenerator.getNextDelay(); + } + + /** Inform that there has been a successful registration for an object. */ + void informRegistrationSuccess(ObjectId objectId) { + // Since registration was successful, we can remove exponential backoff (if any) for the given + // object. + resetDelayGeneratorFor(objectId); + } + + /** + * Inform that there has been a registration failure. + * + * <p>Remove the object from the desired registrations collection whenever there's a failure. We + * don't care if the op that failed was actually an unregister because we never suppress an + * unregister request (even if the object is not in the collection). See + * {@link AndroidListener#issueRegistration}. + */ + public void informRegistrationFailure(ObjectId objectId, boolean isTransient) { + removeDesiredRegistration(objectId); + if (!isTransient) { + // There should be no retries for the object, so remove any backoff state associated with it. + resetDelayGeneratorFor(objectId); + } + } + + /** + * If there is a backoff delay generator for the given object, removes it and sets dirty flag. + */ + private void resetDelayGeneratorFor(ObjectId objectId) { + if (TypedUtil.remove(delayGenerators, objectId) != null) { + isDirty = true; + } + } + + /** Adds the given registration. Returns {@code true} if it was not already tracked. */ + boolean addDesiredRegistration(ObjectId objectId) { + if (desiredRegistrations.add(objectId)) { + isDirty = true; + return true; + } + return false; + } + + /** Removes the given registration. Returns {@code true} if it was actually tracked. */ + boolean removeDesiredRegistration(ObjectId objectId) { + if (desiredRegistrations.remove(objectId)) { + isDirty = true; + return true; + } + return false; + } + + /** + * Resets the {@link #isDirty} flag to {@code false}. Call after marshalling and persisting state. + */ + void resetIsDirty() { + isDirty = false; + } + + @Override + public AndroidListenerProtocol.AndroidListenerState marshal() { + return AndroidListenerProtos.newAndroidListenerState(clientId, requestCodeSeqNum, + delayGenerators, desiredRegistrations); + } + + /** + * Gets the identifier for the current client. Used to determine if registrations commands are + * relevant to this instance. + */ + ByteString getClientId() { + return clientId; + } + + /** Returns {@code true} iff registration is desired for the given object. */ + boolean containsDesiredRegistration(ObjectId objectId) { + return TypedUtil.contains(desiredRegistrations, objectId); + } + + /** + * Returns {@code true} if changes have been made since the last successful call to + * {@link #resetIsDirty}. + */ + boolean getIsDirty() { + return isDirty; + } + + @Override + public int hashCode() { + // Since the client ID is globally unique, it's sufficient as a hashCode. + return clientId.hashCode(); + } + + /** + * Overridden for tests which compare listener states to verify that they have been correctly + * (un)marshalled. We implement equals rather than exposing private data. + */ + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof AndroidListenerState)) { + return false; + } + + AndroidListenerState that = (AndroidListenerState) object; + + return (this.isDirty == that.isDirty) && + (this.requestCodeSeqNum == that.requestCodeSeqNum) && + (this.desiredRegistrations.size() == that.desiredRegistrations.size()) && + (this.desiredRegistrations.containsAll(that.desiredRegistrations)) && + (this.clientId.equals(that.clientId)) && + equals(this.delayGenerators, that.delayGenerators); + } + + /** Compares the contents of two {@link #delayGenerators} maps. */ + private static boolean equals(Map<ObjectId, TiclExponentialBackoffDelayGenerator> x, + Map<ObjectId, TiclExponentialBackoffDelayGenerator> y) { + if (x.size() != y.size()) { + return false; + } + for (Entry<ObjectId, TiclExponentialBackoffDelayGenerator> xEntry : x.entrySet()) { + TiclExponentialBackoffDelayGenerator yGenerator = y.get(xEntry.getKey()); + if ((yGenerator == null) || !xEntry.getValue().marshal().toByteString().equals( + yGenerator.marshal().toByteString())) { + return false; + } + } + return true; + } + + @Override + public String toString() { + return String.format("AndroidListenerState[%s]: isDirty = %b, " + + "desiredRegistrations.size() = %d, delayGenerators.size() = %d, requestCodeSeqNum = %d", + Bytes.toString(clientId), isDirty, desiredRegistrations.size(), delayGenerators.size(), + requestCodeSeqNum); + } + + /** + * Constructs a new globally unique ID for the client. Can be used to determine if commands + * originated from this instance of the listener. + */ + private static ByteString createGloballyUniqueClientId() { + UUID guid = UUID.randomUUID(); + byte[] bytes = new byte[16]; + ByteBuffer buffer = ByteBuffer.wrap(bytes); + buffer.putLong(guid.getLeastSignificantBits()); + buffer.putLong(guid.getMostSignificantBits()); + return ByteString.copyFrom(bytes); + } +} 
diff --git a/src/java/com/google/ipc/invalidation/ticl/InvalidationClientCore.java b/src/java/com/google/ipc/invalidation/ticl/InvalidationClientCore.java index d39799d..13c0822 100644 --- a/src/java/com/google/ipc/invalidation/ticl/InvalidationClientCore.java +++ b/src/java/com/google/ipc/invalidation/ticl/InvalidationClientCore.java 
@@ -26,6 +26,7 @@  import com.google.ipc.invalidation.common.ObjectIdDigestUtils;  import com.google.ipc.invalidation.common.TiclMessageValidator2;  import com.google.ipc.invalidation.external.client.InvalidationListener; +import com.google.ipc.invalidation.external.client.InvalidationListener.RegistrationState;  import com.google.ipc.invalidation.external.client.SystemResources;  import com.google.ipc.invalidation.external.client.SystemResources.Logger;  import com.google.ipc.invalidation.external.client.SystemResources.NetworkChannel; @@ -65,6 +66,7 @@  import com.google.protos.ipc.invalidation.ClientProtocol.InvalidationP;  import com.google.protos.ipc.invalidation.ClientProtocol.ObjectIdP;  import com.google.protos.ipc.invalidation.ClientProtocol.RegistrationP; +import com.google.protos.ipc.invalidation.ClientProtocol.RegistrationP.OpType;  import com.google.protos.ipc.invalidation.ClientProtocol.RegistrationStatus;  import com.google.protos.ipc.invalidation.ClientProtocol.RegistrationSubtree;  import com.google.protos.ipc.invalidation.ClientProtocol.RegistrationSummary; @@ -80,6 +82,7 @@  import java.util.List;  import java.util.Map;  import java.util.Random; +import java.util.Set;  import java.util.logging.Level;     @@ -363,7 +366,7 @@  /** Logger reference into the resources object for cleaner code. */  private final Logger logger;   - /** Storage for the Ticl peristent state. */ + /** Storage for the Ticl persistent state. */  Storage storage;    /** Application callback interface. */ @@ -405,8 +408,6 @@  private ByteString nonce = null;    /** Whether we should send registrations to the server or not. */ - // TODO: Make the server summary in the registration manager nullable - // and replace this variable with a test for whether it's null or not.  private boolean shouldSendRegistrations;    /** Whether the network is online. Assume so when we start. */ @@ -961,11 +962,6 @@  logger.info("Register %s, %s", CommonProtoStrings2.toLazyCompactString(objectIdProto),  regOpType);  objectIdProtos.add(objectIdProto); - // Inform immediately of success so that the application is informed even if the reply - // message from the server is lost. When we get a real ack from the server, we do - // not need to inform the application. - InvalidationListener.RegistrationState regState = convertOpTypeToRegState(regOpType); - listener.informRegistrationStatus(InvalidationClientCore.this, objectId, regState);  }    // Update the registration manager state, then have the protocol client send a message. @@ -1029,22 +1025,6 @@  }    @Override - public void handleNetworkStatusChange(final boolean isOnline) { - // If we're back online and haven't sent a message to the server in a while, send a heartbeat to - // make sure the server knows we're online. - Preconditions.checkState(internalScheduler.isRunningOnThread(), "Not on internal thread"); - boolean wasOnline = this.isOnline; - this.isOnline = isOnline; - if (isOnline && !wasOnline && (internalScheduler.getCurrentTimeMs() > - lastMessageSendTimeMs + config.getOfflineHeartbeatThresholdMs())) { - logger.log( - Level.INFO, "Sending heartbeat after reconnection, previous send was %s ms ago", - internalScheduler.getCurrentTimeMs() - lastMessageSendTimeMs); - sendInfoMessageToServer(false, !registrationManager.isStateInSyncWithServer()); - } - } - - @Override  public void handleMessageSent() {  // The ProtocolHandler just sent a message to the server. If the channel supports offline  // delivery (see the comment in the ClientConfigP), store this time to stable storage. This @@ -1070,6 +1050,21 @@  // Private methods and toString.  //   + void handleNetworkStatusChange(final boolean isOnline) { + // If we're back online and haven't sent a message to the server in a while, send a heartbeat to + // make sure the server knows we're online. + Preconditions.checkState(internalScheduler.isRunningOnThread(), "Not on internal thread"); + boolean wasOnline = this.isOnline; + this.isOnline = isOnline; + if (isOnline && !wasOnline && (internalScheduler.getCurrentTimeMs() > + lastMessageSendTimeMs + config.getOfflineHeartbeatThresholdMs())) { + logger.log(Level.INFO, + "Sending heartbeat after reconnection, previous send was %s ms ago", + internalScheduler.getCurrentTimeMs() - lastMessageSendTimeMs); + sendInfoMessageToServer(false, !registrationManager.isStateInSyncWithServer()); + } + } +  /**  * Handles an {@code incomingMessage} from the data center. If it is valid and addressed to  * this client, dispatches to methods to handle sub-parts of the message; if not, drops the @@ -1144,7 +1139,6 @@  Preconditions.checkArgument(TypedUtil.<ByteString>equals(headerToken, nonce),  "Provided with new token and mismatched nonce: header = %s, nonce = %s",  headerToken, nonce); - setNonce(null);  logger.info("New token being assigned at client: %s, Old = %s",  CommonProtoStrings2.toLazyCompactString(newToken),  CommonProtoStrings2.toLazyCompactString(clientToken)); @@ -1170,7 +1164,20 @@  // We've received a summary from the server, so if we were suppressing  // registrations, we should now allow them to go to the registrar.  shouldSendRegistrations = true; - registrationManager.informServerRegistrationSummary(header.registrationSummary); + + // Pass the registration summary to the registration manager. If we are now in agreement + // with the server and we had any pending operations, we can tell the listener that those + // operations have succeeded. + Set<ProtoWrapper<RegistrationP>> upcalls = + registrationManager.informServerRegistrationSummary(header.registrationSummary); + logger.fine("Receivced new server registration summary (%s); will make %s upcalls", + header.registrationSummary, upcalls.size()); + for (ProtoWrapper<RegistrationP> upcall : upcalls) { + RegistrationP registration = upcall.getProto(); + ObjectId objectId = ProtoConverter.convertFromObjectIdProto(registration.getObjectId()); + RegistrationState regState = convertOpTypeToRegState(registration.getOpType()); + listener.informRegistrationStatus(this, objectId, regState); + }  }  }   @@ -1215,16 +1222,18 @@  boolean wasSuccess = localProcessingStatuses.get(i);  logger.fine("Process reg status: %s", regStatus);   - // Only inform in the case of failure since the success path has already - // been dealt with (the ticl issued informRegistrationStatus immediately - // after receiving the register/unregister call).  ObjectId objectId = ProtoConverter.convertFromObjectIdProto(  regStatus.getRegistration().getObjectId()); - if (!wasSuccess) { + if (wasSuccess) { + // Server operation was both successful and agreed with what the client wanted. + OpType regOpType = regStatus.getRegistration().getOpType(); + InvalidationListener.RegistrationState regState = convertOpTypeToRegState(regOpType); + listener.informRegistrationStatus(InvalidationClientCore.this, objectId, regState); + } else { + // Server operation either failed or disagreed with client's intent (e.g., successful + // unregister, but the client wanted a registration).  String description = CommonProtos2.isSuccess(regStatus.getStatus()) ?  "Registration discrepancy detected" : regStatus.getStatus().getDescription(); - - // Note "success" shows up as transient failure in this scenario.  boolean isPermanent = CommonProtos2.isPermanentFailure(regStatus.getStatus());  listener.informRegistrationFailure(InvalidationClientCore.this, objectId, !isPermanent,  description); @@ -1288,9 +1297,11 @@  }    // If there are any registrations, remove them and issue registration failure. - Collection<ObjectIdP> desiredRegistrations = registrationManager.removeRegisteredObjects(); + Collection<ProtoWrapper<ObjectIdP>> desiredRegistrations = + registrationManager.removeRegisteredObjects();  logger.warning("Issuing failure for %s objects", desiredRegistrations.size()); - for (ObjectIdP objectId : desiredRegistrations) { + for (ProtoWrapper<ObjectIdP> objectIdWrapper : desiredRegistrations) { + ObjectIdP objectId = objectIdWrapper.getProto();  listener.informRegistrationFailure(this,  ProtoConverter.convertFromObjectIdProto(objectId), false, "Auth error: " + description);  } @@ -1313,8 +1324,6 @@  return true;  } else if (nonce != null) {  // Nonce case. - Preconditions.checkState(nonce != null, "Client token and nonce are both null: %s, %s", - clientToken, nonce);  if (!TypedUtil.<ByteString>equals(nonce, parsedMessage.header.token)) {  statistics.recordError(ClientErrorType.NONCE_MISMATCH);  logger.info("Rejecting server message with mismatched nonce: Client = %s, Server = %s", @@ -1328,6 +1337,7 @@  }  }  // Neither token nor nonce; ignore message. + logger.warning("Neither token nor nonce was set in validateToken: %s, %s", clientToken, nonce);  return false;  }   @@ -1352,7 +1362,8 @@  */  private void sendInfoMessageToServer(boolean mustSendPerformanceCounters,  boolean requestServerSummary) { - logger.info("Sending info message to server"); + logger.info("Sending info message to server; request server summary = %s", + requestServerSummary);  Preconditions.checkState(internalScheduler.isRunningOnThread(), "Not on internal thread");    List<SimplePair<String, Integer>> performanceCounters = 
diff --git a/src/java/com/google/ipc/invalidation/ticl/ProtoConverter.java b/src/java/com/google/ipc/invalidation/ticl/ProtoConverter.java index 7204222..ff593f7 100644 --- a/src/java/com/google/ipc/invalidation/ticl/ProtoConverter.java +++ b/src/java/com/google/ipc/invalidation/ticl/ProtoConverter.java 
@@ -18,6 +18,7 @@    import com.google.common.base.Preconditions;  import com.google.ipc.invalidation.common.CommonProtos2; +import com.google.ipc.invalidation.common.TrickleState;  import com.google.ipc.invalidation.external.client.types.Invalidation;  import com.google.ipc.invalidation.external.client.types.ObjectId;  import com.google.protobuf.ByteString; @@ -99,7 +100,13 @@  public static InvalidationP convertToInvalidationProto(Invalidation invalidation) {  Preconditions.checkNotNull(invalidation);  ObjectIdP objectId = convertToObjectIdProto(invalidation.getObjectId()); + + // Invalidations clients do not know about trickle restarts. Every invalidation is allowed + // to suppress earlier invalidations and acks implicitly acknowledge all previous + // invalidations. Therefore the correct semanantics are provided by setting isTrickleRestart to + // true.  return CommonProtos2.newInvalidationP(objectId, invalidation.getVersion(), + TrickleState.RESTART,  invalidation.getPayload() == null ? null : ByteString.copyFrom(invalidation.getPayload()),  null);  } 
diff --git a/src/java/com/google/ipc/invalidation/ticl/ProtocolHandler.java b/src/java/com/google/ipc/invalidation/ticl/ProtocolHandler.java index 8449e08..a005ec9 100644 --- a/src/java/com/google/ipc/invalidation/ticl/ProtocolHandler.java +++ b/src/java/com/google/ipc/invalidation/ticl/ProtocolHandler.java 
@@ -381,9 +381,6 @@  /** Records that a message was sent to the server at the current time. */  void handleMessageSent();   - /** Handles a change in network connectivity. */ - void handleNetworkStatusChange(boolean isOnline); -  /** Returns a summary of the current desired registrations. */  RegistrationSummary getRegistrationSummary();   
diff --git a/src/java/com/google/ipc/invalidation/ticl/RegistrationManager.java b/src/java/com/google/ipc/invalidation/ticl/RegistrationManager.java index b28e51c..534322a 100644 --- a/src/java/com/google/ipc/invalidation/ticl/RegistrationManager.java +++ b/src/java/com/google/ipc/invalidation/ticl/RegistrationManager.java 
@@ -28,6 +28,7 @@  import com.google.ipc.invalidation.util.TypedUtil;  import com.google.protos.ipc.invalidation.ClientProtocol.ObjectIdP;  import com.google.protos.ipc.invalidation.ClientProtocol.RegistrationP; +import com.google.protos.ipc.invalidation.ClientProtocol.RegistrationP.OpType;  import com.google.protos.ipc.invalidation.ClientProtocol.RegistrationStatus;  import com.google.protos.ipc.invalidation.ClientProtocol.RegistrationSubtree;  import com.google.protos.ipc.invalidation.ClientProtocol.RegistrationSummary; @@ -35,7 +36,12 @@    import java.util.ArrayList;  import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet;  import java.util.List; +import java.util.Map; +import java.util.Set;      /** @@ -58,6 +64,20 @@  /** Latest known server registration state summary. */  private ProtoWrapper<RegistrationSummary> lastKnownServerSummary;   + /** + * Map of object ids and operation types for which we have not yet issued any registration-status + * upcall to the listener. We need this so that we can synthesize success upcalls if registration + * sync, rather than a server message, communicates to us that we have a successful + * (un)registration. + * <p> + * This is a map from object id to type, rather than a set of {@code RegistrationP}, because + * a set of {@code RegistrationP} would assume that we always get a response for every operation + * we issue, which isn't necessarily true (i.e., the server might send back an unregistration + * status in response to a registration request). + */ + private final Map<ProtoWrapper<ObjectIdP>, RegistrationP.OpType> pendingOperations = + new HashMap<ProtoWrapper<ObjectIdP>, RegistrationP.OpType>(); +  private final Logger logger;    public RegistrationManager(Logger logger, Statistics statistics, DigestFunction digestFn, @@ -75,6 +95,9 @@  this.lastKnownServerSummary =  ProtoWrapper.of(registrationManagerState.getLastKnownServerSummary());  desiredRegistrations.add(registrationManagerState.getRegistrationsList()); + for (RegistrationP regOp : registrationManagerState.getPendingOperationsList()) { + pendingOperations.put(ProtoWrapper.of(regOp.getObjectId()), regOp.getOpType()); + }  }  }   @@ -115,6 +138,11 @@  /** Perform registration/unregistation for all objects in {@code objectIds}. */  Collection<ObjectIdP> performOperations(Collection<ObjectIdP> objectIds,  RegistrationP.OpType regOpType) { + // Record that we have pending operations on the objects. + for (ObjectIdP objectId : objectIds) { + pendingOperations.put(ProtoWrapper.of(objectId), regOpType); + } + // Update the digest appropriately.  if (regOpType == RegistrationP.OpType.REGISTER) {  return desiredRegistrations.add(objectIds);  } else { @@ -137,56 +165,73 @@    /**  * Handles registration operation statuses from the server. Returns a list of booleans, one per - * registration status that indicates if the registration manager considered the registration - * operation to be successful or not (e.g., if the object was registered and the server - * sent back a reply of successful unregistration, the registration manager will consider that - * as failure since the application's intent is to register that object). + * registration status, that indicates whether the registration operation was both successful and + * agreed with the desired client state (i.e., for each registration status, + * (status.optype == register) == desiredRegistrations.contains(status.objectid)). + * <p> + * REQUIRES: the caller subsequently make an informRegistrationStatus or informRegistrationFailure + * upcall on the listener for each registration in {@code registrationStatuses}.  */  List<Boolean> handleRegistrationStatus(List<RegistrationStatus> registrationStatuses) { - - // Local-processing result code for each element of registrationStatuses. Indicates whether - // the registration status was compatible with the client's desired state (e.g., a successful - // unregister from the server when we desire a registration is incompatible). - List<Boolean> successStatus = new ArrayList<Boolean>(registrationStatuses.size()); + // Local-processing result code for each element of registrationStatuses. + List<Boolean> localStatuses = new ArrayList<Boolean>(registrationStatuses.size());  for (RegistrationStatus registrationStatus : registrationStatuses) {  ObjectIdP objectIdProto = registrationStatus.getRegistration().getObjectId();   + // The object is no longer pending, since we have received a server status for it, so + // remove it from the pendingOperations map. (It may or may not have existed in the map, + // since we can receive spontaneous status messages from the server.) + TypedUtil.remove(pendingOperations, ProtoWrapper.of(objectIdProto)); +  // We start off with the local-processing set as success, then potentially fail.  boolean isSuccess = true;    // if the server operation succeeded, then local processing fails on "incompatibility" as  // defined above.  if (CommonProtos2.isSuccess(registrationStatus.getStatus())) { - boolean inRequestedMap = desiredRegistrations.contains(objectIdProto); - boolean isRegister = + boolean appWantsRegistration = desiredRegistrations.contains(objectIdProto); + boolean isOpRegistration =  registrationStatus.getRegistration().getOpType() == RegistrationP.OpType.REGISTER; - boolean discrepancyExists = isRegister ^ inRequestedMap; + boolean discrepancyExists = isOpRegistration ^ appWantsRegistration;  if (discrepancyExists) { - // Just remove the registration and issue registration failure. - // Caller must issue registration failure to the app so that we find out the actual state - // of the registration. + // Remove the registration and set isSuccess to false, which will cause the caller to + // issue registration-failure to the application.  desiredRegistrations.remove(objectIdProto);  statistics.recordError(ClientErrorType.REGISTRATION_DISCREPANCY);  logger.info("Ticl discrepancy detected: registered = %s, requested = %s. " +  "Removing %s from requested", - isRegister, inRequestedMap, CommonProtoStrings2.toLazyCompactString(objectIdProto)); + isOpRegistration, appWantsRegistration, + CommonProtoStrings2.toLazyCompactString(objectIdProto));  isSuccess = false;  }  } else { - // If the server operation failed, then local processing fails. + // If the server operation failed, then also local processing fails.  desiredRegistrations.remove(objectIdProto);  logger.fine("Removing %s from committed",  CommonProtoStrings2.toLazyCompactString(objectIdProto));  isSuccess = false;  } - successStatus.add(isSuccess); + localStatuses.add(isSuccess);  } - return successStatus; + return localStatuses;  }   - /** Removes all the registrations in this manager and returns the list. */ - Collection<ObjectIdP> removeRegisteredObjects() { - return desiredRegistrations.removeAll(); + /** + * Removes all desired registrations and pending operations. Returns all object ids + * that were affected. + * <p> + * REQUIRES: the caller issue a permanent failure upcall to the listener for all returned object + * ids. + */ + Collection<ProtoWrapper<ObjectIdP>> removeRegisteredObjects() { + int numObjects = desiredRegistrations.size() + pendingOperations.size(); + Set<ProtoWrapper<ObjectIdP>> failureCalls = new HashSet<ProtoWrapper<ObjectIdP>>(numObjects); + for (ObjectIdP objectId : desiredRegistrations.removeAll()) { + failureCalls.add(ProtoWrapper.of(objectId)); + } + failureCalls.addAll(pendingOperations.keySet()); + pendingOperations.clear(); + return failureCalls;  }    // @@ -199,11 +244,33 @@  desiredRegistrations.getDigest());  }   - /** Informs the manager of a new registration state summary from the server. */ - void informServerRegistrationSummary(RegistrationSummary regSummary) { + /** + * Informs the manager of a new registration state summary from the server. + * Returns a possibly-empty map of <object-id, reg-op-type>. For each entry in the map, + * the caller should make an inform-registration-status upcall on the listener. + */ + Set<ProtoWrapper<RegistrationP>> informServerRegistrationSummary( + RegistrationSummary regSummary) {  if (regSummary != null) {  this.lastKnownServerSummary = ProtoWrapper.of(regSummary);  } + if (isStateInSyncWithServer()) { + // If we are now in sync with the server, then the caller should make inform-reg-status + // upcalls for all operations that we had pending, if any; they are also no longer pending. + Set<ProtoWrapper<RegistrationP>> upcallsToMake = + new HashSet<ProtoWrapper<RegistrationP>>(pendingOperations.size()); + for (Map.Entry<ProtoWrapper<ObjectIdP>, RegistrationP.OpType> entry : + pendingOperations.entrySet()) { + ObjectIdP objectId = entry.getKey().getProto(); + boolean isReg = entry.getValue() == OpType.REGISTER; + upcallsToMake.add(ProtoWrapper.of(CommonProtos2.newRegistrationP(objectId, isReg))); + } + pendingOperations.clear(); + return upcallsToMake; + } else { + // If we are not in sync with the server, then the caller should make no upcalls. + return Collections.emptySet(); + }  }    /** @@ -225,6 +292,12 @@  RegistrationManagerStateP.Builder builder = RegistrationManagerStateP.newBuilder();  builder.setLastKnownServerSummary(lastKnownServerSummary.getProto());  builder.addAllRegistrations(desiredRegistrations.getElements(EMPTY_PREFIX, 0)); + for (Map.Entry<ProtoWrapper<ObjectIdP>, RegistrationP.OpType> pendingOp : + pendingOperations.entrySet()) { + ObjectIdP objectId = pendingOp.getKey().getProto(); + boolean isReg = pendingOp.getValue() == OpType.REGISTER; + builder.addPendingOperations(CommonProtos2.newRegistrationP(objectId, isReg)); + }  return builder.build();  }  } 
diff --git a/src/java/com/google/ipc/invalidation/ticl/TiclExponentialBackoffDelayGenerator.java b/src/java/com/google/ipc/invalidation/ticl/TiclExponentialBackoffDelayGenerator.java index d5fe67a..a75cfed 100644 --- a/src/java/com/google/ipc/invalidation/ticl/TiclExponentialBackoffDelayGenerator.java +++ b/src/java/com/google/ipc/invalidation/ticl/TiclExponentialBackoffDelayGenerator.java 
@@ -50,7 +50,6 @@  marshalledState.getInRetryMode());  }   -  @Override  public ExponentialBackoffState marshal() {  return ExponentialBackoffState.newBuilder() 
diff --git a/src/java/com/google/ipc/invalidation/ticl/android/c2dm/WakeLockManager.java b/src/java/com/google/ipc/invalidation/ticl/android/c2dm/WakeLockManager.java index 43090b8..823c20f 100644 --- a/src/java/com/google/ipc/invalidation/ticl/android/c2dm/WakeLockManager.java +++ b/src/java/com/google/ipc/invalidation/ticl/android/c2dm/WakeLockManager.java 
@@ -57,6 +57,8 @@    /** Returns the wake lock manager. */  public static WakeLockManager getInstance(Context context) { + Preconditions.checkNotNull(context); + Preconditions.checkNotNull(context.getApplicationContext());  synchronized (LOCK) {  if (theManager == null) {  theManager = new WakeLockManager(context.getApplicationContext()); 
diff --git a/src/java/com/google/ipc/invalidation/ticl/android2/AndroidIntentProtocolValidator.java b/src/java/com/google/ipc/invalidation/ticl/android2/AndroidIntentProtocolValidator.java index 975a962..b12f406 100644 --- a/src/java/com/google/ipc/invalidation/ticl/android2/AndroidIntentProtocolValidator.java +++ b/src/java/com/google/ipc/invalidation/ticl/android2/AndroidIntentProtocolValidator.java 
@@ -61,8 +61,6 @@  *  */  public final class AndroidIntentProtocolValidator extends ProtoValidator { - // TODO: rename to AndroidIntentProtocolValidator. -  /** Validation for composite (major/minor) versions. */  static final MessageInfo VERSION = new MessageInfo(ClientProtocolAccessor.VERSION_ACCESSOR,  FieldInfo.newRequired(VersionAccessor.MAJOR_VERSION), 
diff --git a/src/java/com/google/ipc/invalidation/ticl/android2/AndroidInvalidationClientImpl.java b/src/java/com/google/ipc/invalidation/ticl/android2/AndroidInvalidationClientImpl.java index 4d07ada..e8b3d4b 100644 --- a/src/java/com/google/ipc/invalidation/ticl/android2/AndroidInvalidationClientImpl.java +++ b/src/java/com/google/ipc/invalidation/ticl/android2/AndroidInvalidationClientImpl.java 
@@ -61,10 +61,6 @@  /** Class implementing the application listener stub (allows overriding default for tests). */  static Class<? extends Service> listenerServiceClassForTest = null;   - /** Name of the class that implements the application listener stub in production code. */ - private static final String LISTENER_STUB_CLASS_NAME = - "com.google.ipc.invalidation.ticl.android2.AndroidInvalidationListenerStub"; -  /**  * {@link InvalidationListener} implementation that forwards all calls to a remote listener  * using Android intents. @@ -154,11 +150,12 @@  }    /** - * Sends {@code intent} to the real listener using the {@link AndroidInvalidationListenerStub}. + * Sends {@code intent} to the real listener via the listener intent service class.  */  static void issueIntent(Context context, Intent intent) {  intent.setClassName(context, (listenerServiceClassForTest != null) ? - listenerServiceClassForTest.getName() : LISTENER_STUB_CLASS_NAME); + listenerServiceClassForTest.getName() : + new AndroidTiclManifest(context).getListenerServiceClass());  context.startService(intent);  }   
diff --git a/src/java/com/google/ipc/invalidation/ticl/android2/AndroidInvalidationListenerIntentMapper.java b/src/java/com/google/ipc/invalidation/ticl/android2/AndroidInvalidationListenerIntentMapper.java new file mode 100644 index 0000000..e65a692 --- /dev/null +++ b/src/java/com/google/ipc/invalidation/ticl/android2/AndroidInvalidationListenerIntentMapper.java 
@@ -0,0 +1,160 @@ +/* + * Copyright 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.ipc.invalidation.ticl.android2; + +import com.google.ipc.invalidation.external.client.InvalidationClient; +import com.google.ipc.invalidation.external.client.InvalidationListener; +import com.google.ipc.invalidation.external.client.InvalidationListener.RegistrationState; +import com.google.ipc.invalidation.external.client.android.service.AndroidLogger; +import com.google.ipc.invalidation.external.client.types.AckHandle; +import com.google.ipc.invalidation.external.client.types.ErrorInfo; +import com.google.ipc.invalidation.ticl.ProtoConverter; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protos.ipc.invalidation.AndroidService.ListenerUpcall; +import com.google.protos.ipc.invalidation.AndroidService.ListenerUpcall.ErrorUpcall; +import com.google.protos.ipc.invalidation.AndroidService.ListenerUpcall.InvalidateUpcall; +import com.google.protos.ipc.invalidation.AndroidService.ListenerUpcall.RegistrationFailureUpcall; +import com.google.protos.ipc.invalidation.AndroidService.ListenerUpcall.RegistrationStatusUpcall; +import com.google.protos.ipc.invalidation.AndroidService.ListenerUpcall.ReissueRegistrationsUpcall; + +import android.content.Context; +import android.content.Intent; + +import java.util.Arrays; + + +/** + * Routes intents to the appropriate methods in {@link InvalidationListener}. Typically, an instance + * of the mapper should be created in {@code IntentService#onCreate} and the {@link #handleIntent} + * method called in {@code IntentService#onHandleIntent}. + * + */ +public final class AndroidInvalidationListenerIntentMapper { + + /** The logger. */ + private final AndroidLogger logger = AndroidLogger.forPrefix(""); + + private final AndroidIntentProtocolValidator validator = + new AndroidIntentProtocolValidator(logger); + + /** Client passed to the listener (supports downcalls). */ + public final InvalidationClient client; + + /** Listener to which intents are routed. */ + private final InvalidationListener listener; + + /** + * Initializes + * + * @param listener the listener to which intents should be routed + * @param context the context used by the listener to issue downcalls to the TICL + */ + public AndroidInvalidationListenerIntentMapper(InvalidationListener listener, Context context) { + client = new AndroidInvalidationClientStub(context, logger); + this.listener = listener; + } + + /** + * Handles a listener upcall by decoding the protocol buffer in {@code intent} and dispatching + * to the appropriate method on the {@link #listener}. + */ + public void handleIntent(Intent intent) { + // TODO: use wakelocks + + // Unmarshall the arguments from the Intent and make the appropriate call on the listener. + ListenerUpcall upcall = tryParseIntent(intent); + if (upcall == null) { + return; + } + + if (upcall.hasReady()) { + listener.ready(client); + } else if (upcall.hasInvalidate()) { + // Handle all invalidation-related upcalls on a common path, since they require creating + // an AckHandleP. + onInvalidateUpcall(upcall, listener); + } else if (upcall.hasRegistrationStatus()) { + RegistrationStatusUpcall regStatus = upcall.getRegistrationStatus(); + listener.informRegistrationStatus(client, + ProtoConverter.convertFromObjectIdProto(regStatus.getObjectId()), + regStatus.getIsRegistered() ? + RegistrationState.REGISTERED : RegistrationState.UNREGISTERED); + } else if (upcall.hasRegistrationFailure()) { + RegistrationFailureUpcall failure = upcall.getRegistrationFailure(); + listener.informRegistrationFailure(client, + ProtoConverter.convertFromObjectIdProto(failure.getObjectId()), + failure.getTransient(), + failure.getMessage()); + } else if (upcall.hasReissueRegistrations()) { + ReissueRegistrationsUpcall reissueRegs = upcall.getReissueRegistrations(); + listener.reissueRegistrations(client, reissueRegs.getPrefix().toByteArray(), + reissueRegs.getLength()); + } else if (upcall.hasError()) { + ErrorUpcall error = upcall.getError(); + ErrorInfo errorInfo = ErrorInfo.newInstance(error.getErrorCode(), error.getIsTransient(), + error.getErrorMessage(), null); + listener.informError(client, errorInfo); + } else { + logger.warning("Dropping listener Intent with unknown call: %s", upcall); + } + } + + /** + * Handles an invalidation-related listener {@code upcall} by dispatching to the appropriate + * method on an instance of {@link #listenerClass}. + */ + private void onInvalidateUpcall(ListenerUpcall upcall, InvalidationListener listener) { + InvalidateUpcall invalidate = upcall.getInvalidate(); + AckHandle ackHandle = AckHandle.newInstance(invalidate.getAckHandle().toByteArray()); + if (invalidate.hasInvalidation()) { + listener.invalidate(client, + ProtoConverter.convertFromInvalidationProto(invalidate.getInvalidation()), + ackHandle); + } else if (invalidate.hasInvalidateAll()) { + listener.invalidateAll(client, ackHandle); + } else if (invalidate.hasInvalidateUnknown()) { + listener.invalidateUnknownVersion(client, + ProtoConverter.convertFromObjectIdProto(invalidate.getInvalidateUnknown()), ackHandle); + } else { + throw new RuntimeException("Invalid invalidate upcall: " + invalidate); + } + } + + /** + * Returns a valid {@link ListenerUpcall} from {@code intent}, or {@code null} if one + * could not be parsed. + */ + private ListenerUpcall tryParseIntent(Intent intent) { + if (intent == null) { + return null; + } + byte[] upcallBytes = intent.getByteArrayExtra(ProtocolIntents.LISTENER_UPCALL_KEY); + if (upcallBytes == null) { + return null; + } + try { + ListenerUpcall upcall = ListenerUpcall.parseFrom(upcallBytes); + if (!validator.isListenerUpcallValid(upcall)) { + logger.warning("Ignoring invalid listener upcall: %s", upcall); + return null; + } + return upcall; + } catch (InvalidProtocolBufferException exception) { + logger.severe("Could not parse listener upcall from %s", Arrays.toString(upcallBytes)); + return null; + } + } +} 
diff --git a/src/java/com/google/ipc/invalidation/ticl/android2/AndroidInvalidationListenerStub.java b/src/java/com/google/ipc/invalidation/ticl/android2/AndroidInvalidationListenerStub.java index 1cc695c..247309d 100644 --- a/src/java/com/google/ipc/invalidation/ticl/android2/AndroidInvalidationListenerStub.java +++ b/src/java/com/google/ipc/invalidation/ticl/android2/AndroidInvalidationListenerStub.java 
@@ -16,26 +16,12 @@    package com.google.ipc.invalidation.ticl.android2;   -import com.google.ipc.invalidation.external.client.InvalidationClient;  import com.google.ipc.invalidation.external.client.InvalidationListener; -import com.google.ipc.invalidation.external.client.InvalidationListener.RegistrationState;  import com.google.ipc.invalidation.external.client.android.service.AndroidLogger; -import com.google.ipc.invalidation.external.client.types.AckHandle; -import com.google.ipc.invalidation.external.client.types.ErrorInfo; -import com.google.ipc.invalidation.ticl.ProtoConverter; -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.protos.ipc.invalidation.AndroidService.ListenerUpcall; -import com.google.protos.ipc.invalidation.AndroidService.ListenerUpcall.ErrorUpcall; -import com.google.protos.ipc.invalidation.AndroidService.ListenerUpcall.InvalidateUpcall; -import com.google.protos.ipc.invalidation.AndroidService.ListenerUpcall.RegistrationFailureUpcall; -import com.google.protos.ipc.invalidation.AndroidService.ListenerUpcall.RegistrationStatusUpcall; -import com.google.protos.ipc.invalidation.AndroidService.ListenerUpcall.ReissueRegistrationsUpcall;    import android.app.IntentService;  import android.content.Intent;   -import java.util.Arrays; -    /**  * Class implementing the {@link InvalidationListener} in the application using the client. @@ -47,35 +33,31 @@  public class AndroidInvalidationListenerStub extends IntentService {  /* This class needs to be public so that the Android runtime can start it as a service. */   - /** {@link InvalidationClient} that will be provided to listener upcalls. */ - private InvalidationClient client; - - /** Class against instances of which listener calls will be made. */ - private Class<? extends InvalidationListener> listenerClass; -  private final AndroidLogger logger = AndroidLogger.forPrefix("");   - private final AndroidIntentProtocolValidator validator = - new AndroidIntentProtocolValidator(logger); + /** The mapper used to route intents to the invalidation listener. */ + private AndroidInvalidationListenerIntentMapper intentMapper;    public AndroidInvalidationListenerStub() { - super("AndroidInvalidationListener"); + super("");  }   - @SuppressWarnings("unchecked")  @Override  public void onCreate() {  super.onCreate(); + InvalidationListener listener = createListener(getListenerClass()); + intentMapper = new AndroidInvalidationListenerIntentMapper(listener, getApplicationContext()); + } + + @SuppressWarnings("unchecked") + private Class<? extends InvalidationListener> getListenerClass() {  try {  // Find the listener class that the application wants to use to receive upcalls. - this.listenerClass = (Class<? extends InvalidationListener>) + return (Class<? extends InvalidationListener>)  Class.forName(new AndroidTiclManifest(this).getListenerClass());  } catch (ClassNotFoundException exception) {  throw new RuntimeException("Invalid listener class", exception);  } - // Create a stub back to the Ticl service; we will provide this as the client parameter in - // listener upcalls. - this.client = new AndroidInvalidationClientStub(this, logger);  }    /** @@ -83,96 +65,19 @@  * to the appropriate method on an instance of {@link #listenerClass}.  */  @Override - protected void onHandleIntent(Intent intent) { - // TODO: use wakelocks + public void onHandleIntent(Intent intent) { + logger.fine("onHandleIntent({0})", AndroidStrings.toLazyCompactString(intent)); + intentMapper.handleIntent(intent); + }   + private InvalidationListener createListener(Class<? extends InvalidationListener> listenerClass) {  // Create an instance of the application listener class to handle the upcall. - InvalidationListener listener;  try { - listener = listenerClass.newInstance(); + return listenerClass.newInstance();  } catch (InstantiationException exception) {  throw new RuntimeException("Could not create listener", exception);  } catch (IllegalAccessException exception) {  throw new RuntimeException("Could not create listener", exception);  } - // Unmarshall the arguments from the Intent and make the appropriate call on the listener. - ListenerUpcall upcall = tryParseIntent(intent); - if (upcall == null) { - return; - } - - if (upcall.hasReady()) { - listener.ready(client); - } else if (upcall.hasInvalidate()) { - // Handle all invalidation-related upcalls on a common path, since they require creating - // an AckHandleP. - onInvalidateUpcall(upcall, listener); - } else if (upcall.hasRegistrationStatus()) { - RegistrationStatusUpcall regStatus = upcall.getRegistrationStatus(); - listener.informRegistrationStatus(client, - ProtoConverter.convertFromObjectIdProto(regStatus.getObjectId()), - regStatus.getIsRegistered() ? - RegistrationState.REGISTERED : RegistrationState.UNREGISTERED); - } else if (upcall.hasRegistrationFailure()) { - RegistrationFailureUpcall failure = upcall.getRegistrationFailure(); - listener.informRegistrationFailure(client, - ProtoConverter.convertFromObjectIdProto(failure.getObjectId()), - failure.getTransient(), - failure.getMessage()); - } else if (upcall.hasReissueRegistrations()) { - ReissueRegistrationsUpcall reissueRegs = upcall.getReissueRegistrations(); - listener.reissueRegistrations(client, reissueRegs.getPrefix().toByteArray(), - reissueRegs.getLength()); - } else if (upcall.hasError()) { - ErrorUpcall error = upcall.getError(); - ErrorInfo errorInfo = ErrorInfo.newInstance(error.getErrorCode(), error.getIsTransient(), - error.getErrorMessage(), null); - listener.informError(client, errorInfo); - } else { - logger.warning("Dropping listener Intent with unknown call: %s", upcall); - } - } - - /** - * Handles an invalidation-related listener {@code upcall} by dispatching to the appropriate - * method on an instance of {@link #listenerClass}. - */ - private void onInvalidateUpcall(ListenerUpcall upcall, InvalidationListener listener) { - InvalidateUpcall invalidate = upcall.getInvalidate(); - AckHandle ackHandle = AckHandle.newInstance(invalidate.getAckHandle().toByteArray()); - if (invalidate.hasInvalidation()) { - listener.invalidate(client, - ProtoConverter.convertFromInvalidationProto(invalidate.getInvalidation()), - ackHandle); - } else if (invalidate.hasInvalidateAll()) { - listener.invalidateAll(client, ackHandle); - } else if (invalidate.hasInvalidateUnknown()) { - listener.invalidateUnknownVersion(client, - ProtoConverter.convertFromObjectIdProto(invalidate.getInvalidateUnknown()), ackHandle); - } else { - throw new RuntimeException("Invalid invalidate upcall: " + invalidate); - } - } - - /** - * Returns a valid {@link ListenerUpcall} from {@code intent}, or {@code null} if one - * could not be parsed. - */ - private ListenerUpcall tryParseIntent(Intent intent) { - if (intent == null) { - return null; - } - byte[] upcallBytes = intent.getByteArrayExtra(ProtocolIntents.LISTENER_UPCALL_KEY); - try { - ListenerUpcall upcall = ListenerUpcall.parseFrom(upcallBytes); - if (!validator.isListenerUpcallValid(upcall)) { - logger.warning("Ignoring invalid listener upcall: %s", upcall); - return null; - } - return upcall; - } catch (InvalidProtocolBufferException exception) { - logger.severe("Could not parse listener upcall from %s", Arrays.toString(upcallBytes)); - return null; - }  }  } 
diff --git a/src/java/com/google/ipc/invalidation/ticl/android2/AndroidStrings.java b/src/java/com/google/ipc/invalidation/ticl/android2/AndroidStrings.java new file mode 100644 index 0000000..4bc6c5b --- /dev/null +++ b/src/java/com/google/ipc/invalidation/ticl/android2/AndroidStrings.java 
@@ -0,0 +1,390 @@ +/* + * Copyright 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.ipc.invalidation.ticl.android2; + +import com.google.common.base.Joiner; +import com.google.ipc.invalidation.common.CommonProtoStrings2; +import com.google.ipc.invalidation.util.TextBuilder; +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protos.ipc.invalidation.AndroidService.AndroidNetworkSendRequest; +import com.google.protos.ipc.invalidation.AndroidService.AndroidSchedulerEvent; +import com.google.protos.ipc.invalidation.AndroidService.ClientDowncall; +import com.google.protos.ipc.invalidation.AndroidService.ClientDowncall.AckDowncall; +import com.google.protos.ipc.invalidation.AndroidService.ClientDowncall.RegistrationDowncall; +import com.google.protos.ipc.invalidation.AndroidService.InternalDowncall; +import com.google.protos.ipc.invalidation.AndroidService.InternalDowncall.CreateClient; +import com.google.protos.ipc.invalidation.AndroidService.InternalDowncall.NetworkStatus; +import com.google.protos.ipc.invalidation.AndroidService.InternalDowncall.ServerMessage; +import com.google.protos.ipc.invalidation.AndroidService.ListenerUpcall; +import com.google.protos.ipc.invalidation.AndroidService.ListenerUpcall.ErrorUpcall; +import com.google.protos.ipc.invalidation.AndroidService.ListenerUpcall.InvalidateUpcall; +import com.google.protos.ipc.invalidation.AndroidService.ListenerUpcall.RegistrationFailureUpcall; +import com.google.protos.ipc.invalidation.AndroidService.ListenerUpcall.RegistrationStatusUpcall; +import com.google.protos.ipc.invalidation.AndroidService.ListenerUpcall.ReissueRegistrationsUpcall; +import com.google.protos.ipc.invalidation.Client.AckHandleP; +import com.google.protos.ipc.invalidation.ClientProtocol.ObjectIdP; + +import android.content.Intent; + +import java.util.List; + + +/** + * Utilities to format Android protocol buffers and intents as compact strings suitable for logging. + * By convention, methods take a {@link TextBuilder} and the object to format and return the + * builder. Null object arguments are permitted. + * + * <p>{@link #toCompactString} methods immediately append a description of the object to the given + * {@link TextBuilder}s. {@link #toLazyCompactString} methods return an object that defers the work + * of formatting the provided argument until {@link Object#toString} is called. + * + */ +public class AndroidStrings { + + /** + * String to return when the argument is unknown (suggests a new protocol field or invalid + * proto). + */ + static final String UNKNOWN_MESSAGE = "UNKNOWN@AndroidStrings"; + + /** + * String to return when there is an error formatting an argument. + */ + static final String ERROR_MESSAGE = "ERROR@AndroidStrings"; + + /** + * Returns an object that lazily evaluates {@link #toCompactString} when {@link Object#toString} + * is called. + */ + public static Object toLazyCompactString(final Intent intent) { + return new Object() { + @Override + public String toString() { + TextBuilder builder = new TextBuilder(); + AndroidStrings.toCompactString(builder, intent); + return builder.toString(); + } + }; + } + + /** Appends a description of the given client {@code downcall} to the given {@code builder}. */ + public static TextBuilder toCompactString(TextBuilder builder, + ClientDowncall downcall) { + if (downcall == null) { + return builder; + } + builder.append(ProtocolIntents.CLIENT_DOWNCALL_KEY).append("::"); + if (downcall.hasStart()) { + builder.append("start()"); + } else if (downcall.hasStop()) { + builder.append("stop()"); + } else if (downcall.hasAck()) { + toCompactString(builder, downcall.getAck()); + } else if (downcall.hasRegistrations()) { + toCompactString(builder, downcall.getRegistrations()); + } else { + builder.append(UNKNOWN_MESSAGE); + } + return builder; + } + + /** Appends a description of the given {@code ack} downcall to the given {@code builder}. */ + public static TextBuilder toCompactString(TextBuilder builder, AckDowncall ack) { + if (ack == null) { + return builder; + } + builder.append("ack("); + serializedAckHandleToCompactString(builder, ack.getAckHandle()); + return builder.append(")"); + } + + /** + * Appends a description of the given {@code registration} downcall to the given {@code builder}. + */ + public static TextBuilder toCompactString(TextBuilder builder, + RegistrationDowncall registration) { + if (registration == null) { + return builder; + } + List<ObjectIdP> objects; + if (registration.getRegistrationsCount() > 0) { + builder.append("register("); + objects = registration.getRegistrationsList(); + } else { + builder.append("unregister("); + objects = registration.getUnregistrationsList(); + } + return CommonProtoStrings2.toCompactStringForObjectIds(builder, objects).append(")"); + } + + /** Appends a description of the given internal {@code downcall} to the given {@code builder}. */ + public static TextBuilder toCompactString(TextBuilder builder, + InternalDowncall downcall) { + if (downcall == null) { + return builder; + } + builder.append(ProtocolIntents.INTERNAL_DOWNCALL_KEY).append("::"); + if (downcall.hasServerMessage()) { + toCompactString(builder, downcall.getServerMessage()); + } else if (downcall.hasNetworkStatus()) { + toCompactString(builder, downcall.getNetworkStatus()); + } else if (downcall.hasNetworkAddrChange()) { + builder.append("newtworkAddrChange()"); + } else if (downcall.hasCreateClient()) { + toCompactString(builder, downcall.getCreateClient()); + } else { + builder.append(UNKNOWN_MESSAGE); + } + return builder; + } + + /** Appends a description of the given {@code serverMessage} to the given {@code builder}. */ + public static TextBuilder toCompactString(TextBuilder builder, + ServerMessage serverMessage) { + if (serverMessage == null) { + return builder; + } + return builder.append("serverMessage(").append(serverMessage.getData()).append(")"); + } + + /** Appends a description of the given {@code networkStatus} to the given {@code builder}. */ + public static TextBuilder toCompactString(TextBuilder builder, + NetworkStatus networkStatus) { + if (networkStatus == null) { + return builder; + } + return builder.append("networkStatus(isOnline = ").append(networkStatus.getIsOnline()) + .append(")"); + } + + /** + * Appends a description of the given {@code createClient} command to the given {@code builder}. + */ + public static TextBuilder toCompactString(TextBuilder builder, + CreateClient createClient) { + if (createClient == null) { + return builder; + } + return builder.append("createClient(type = ").append(createClient.getClientType()) + .append(", name = ").append(createClient.getClientName()).append(", skipStartForTest = ") + .append(createClient.getSkipStartForTest()).append(")"); + } + + /** Appends a description of the given listener {@code upcall} to the given {@code builder}. */ + public static TextBuilder toCompactString(TextBuilder builder, + ListenerUpcall upcall) { + if (upcall == null) { + return builder; + } + builder.append(ProtocolIntents.LISTENER_UPCALL_KEY).append("::"); + if (upcall.hasReady()) { + builder.append(".ready()"); + } else if (upcall.hasInvalidate()) { + toCompactString(builder, upcall.getInvalidate()); + } else if (upcall.hasRegistrationStatus()) { + toCompactString(builder, upcall.getRegistrationStatus()); + } else if (upcall.hasRegistrationFailure()) { + toCompactString(builder, upcall.getRegistrationFailure()); + } else if (upcall.hasReissueRegistrations()) { + toCompactString(builder, upcall.getReissueRegistrations()); + } else if (upcall.hasError()) { + toCompactString(builder, upcall.getError()); + } else { + builder.append(UNKNOWN_MESSAGE); + } + return builder; + } + + /** Appends a description of the given {@code invalidate} command to the given {@code builder}. */ + public static TextBuilder toCompactString(TextBuilder builder, + InvalidateUpcall invalidate) { + if (invalidate == null) { + return builder; + } + builder.append("invalidate(ackHandle = "); + serializedAckHandleToCompactString(builder, invalidate.getAckHandle()); + builder.append(", "); + if (invalidate.hasInvalidation()) { + CommonProtoStrings2.toCompactString(builder, invalidate.getInvalidation()); + } else if (invalidate.getInvalidateAll()) { + builder.append("ALL"); + } else if (invalidate.hasInvalidateUnknown()) { + builder.append("UNKNOWN: "); + CommonProtoStrings2.toCompactString(builder, invalidate.getInvalidateUnknown()); + } + return builder.append(")"); + } + + /** Appends a description of the given {@code status} upcall to the given {@code builder}. */ + public static TextBuilder toCompactString(TextBuilder builder, + RegistrationStatusUpcall status) { + if (status == null) { + return builder; + } + builder.append("registrationStatus(objectId = "); + CommonProtoStrings2.toCompactString(builder, status.getObjectId()); + return builder.append(", isRegistered = ").append(status.getIsRegistered()).append(")"); + } + + /** Appends a description of the given {@code failure} upcall to the given {@code builder}. */ + public static TextBuilder toCompactString(TextBuilder builder, + RegistrationFailureUpcall failure) { + if (failure == null) { + return builder; + } + builder.append("registrationFailure(objectId = "); + CommonProtoStrings2.toCompactString(builder, failure.getObjectId()); + return builder.append(", isTransient = ").append(failure.getTransient()).append(")"); + } + + /** + * Appends a description of the given {@code reissue} registrations upcall to the given + * {@code builder}. + */ + public static TextBuilder toCompactString(TextBuilder builder, + ReissueRegistrationsUpcall reissue) { + if (reissue == null) { + return builder; + } + builder.append("reissueRegistrations(prefix = "); + return builder.append(reissue.getPrefix()).append(", length = ").append(reissue.getLength()) + .append(")"); + } + + /** Appends a description of the given {@code error} upcall to the given {@code builder}. */ + public static TextBuilder toCompactString(TextBuilder builder, ErrorUpcall error) { + if (error == null) { + return builder; + } + return builder.append("error(code = ").append(error.getErrorCode()).append(", message = ") + .append(error.getErrorMessage()).append(", isTransient = ").append(error.getIsTransient()) + .append(")"); + } + + /** Appends a description of the given {@code request} to the given {@code builder}. */ + public static TextBuilder toCompactString(TextBuilder builder, + AndroidNetworkSendRequest request) { + if (request == null) { + return builder; + } + return builder.append(ProtocolIntents.OUTBOUND_MESSAGE_KEY).append("(") + .append(request.getMessage()).append(")"); + } + + /** Appends a description of the given (@code event} to the given {@code builder}. */ + public static TextBuilder toCompactString(TextBuilder builder, + AndroidSchedulerEvent event) { + if (event == null) { + return builder; + } + return builder.append(ProtocolIntents.SCHEDULER_KEY).append("(eventName = ") + .append(event.getEventName()).append(", ticlId = ").append(event.getTiclId()).append(")"); + } + + /** See spec in implementation notes. */ + public static TextBuilder toCompactString(TextBuilder builder, AckHandleP ackHandle) { + if (ackHandle == null) { + return builder; + } + return CommonProtoStrings2.toCompactString(builder.appendFormat("AckHandle: "), + ackHandle.getInvalidation()); + } + + /** + * Appends a description of the given {@code intent} to the given {@code builder}. If the intent + * includes some recognized extras, formats the extra context as well. + */ + public static TextBuilder toCompactString(TextBuilder builder, Intent intent) { + if (intent == null) { + return builder; + } + builder.append("intent("); + try { + if (!tryParseExtra(builder, intent)) { + builder.append(UNKNOWN_MESSAGE).append(", extras = ") + .append(Joiner.on(", ").join(intent.getExtras().keySet())); + } + } catch (InvalidProtocolBufferException exception) { + builder.append(ERROR_MESSAGE).append(" : ").append(exception); + } + return builder.append(")"); + } + + /** Appends a description of any known extra or appends 'UNKNOWN' if none are recognized. */ + private static boolean tryParseExtra(TextBuilder builder, Intent intent) + throws InvalidProtocolBufferException { + byte[] data; + + data = intent.getByteArrayExtra(ProtocolIntents.SCHEDULER_KEY); + if (data != null) { + AndroidSchedulerEvent schedulerEvent = AndroidSchedulerEvent.parseFrom(data); + toCompactString(builder, schedulerEvent); + return true; + } + + data = intent.getByteArrayExtra(ProtocolIntents.OUTBOUND_MESSAGE_KEY); + if (data != null) { + AndroidNetworkSendRequest outboundMessage = AndroidNetworkSendRequest.parseFrom(data); + toCompactString(builder, outboundMessage); + return true; + } + + data = intent.getByteArrayExtra(ProtocolIntents.LISTENER_UPCALL_KEY); + if (data != null) { + ListenerUpcall upcall = ListenerUpcall.parseFrom(data); + toCompactString(builder, upcall); + return true; + } + + data = intent.getByteArrayExtra(ProtocolIntents.INTERNAL_DOWNCALL_KEY); + if (data != null) { + InternalDowncall internalDowncall = InternalDowncall.parseFrom(data); + toCompactString(builder, internalDowncall); + return true; + } + + data = intent.getByteArrayExtra(ProtocolIntents.CLIENT_DOWNCALL_KEY); + if (data != null) { + ClientDowncall clientDowncall = ClientDowncall.parseFrom(data); + toCompactString(builder, clientDowncall); + return true; + } + + // Didn't recognize any intents. + return false; + } + + /** Given serialized form of an ack handle, appends description to {@code builder}. */ + private static TextBuilder serializedAckHandleToCompactString( + TextBuilder builder, ByteString serialized) { + if (serialized == null) { + return builder; + } + // The ack handle is supposed by an AckHandleP! + try { + AckHandleP ackHandle = AckHandleP.parseFrom(serialized); + return toCompactString(builder, ackHandle); + } catch (InvalidProtocolBufferException exception) { + // But it wasn't... Just log the raw bytes. + return builder.append(serialized); + } + } + + private AndroidStrings() { + // Avoid instantiation. + } +} 
diff --git a/src/java/com/google/ipc/invalidation/ticl/android2/AndroidTiclManifest.java b/src/java/com/google/ipc/invalidation/ticl/android2/AndroidTiclManifest.java index 65a7a87..09d69ca 100644 --- a/src/java/com/google/ipc/invalidation/ticl/android2/AndroidTiclManifest.java +++ b/src/java/com/google/ipc/invalidation/ticl/android2/AndroidTiclManifest.java 
@@ -35,7 +35,8 @@  public class AndroidTiclManifest {  /**  * Name of the {@code <application>} metadata element whose value gives the Java class that - * implements the application {@code InvalidationListener}. Must always be set. + * implements the application {@code InvalidationListener}. Must be set if + * {@link #LISTENER_SERVICE_NAME_KEY} is not set.  */  private static final String LISTENER_NAME_KEY = "ipc.invalidation.ticl.listener_class";   @@ -45,12 +46,21 @@  */  private static final String TICL_SERVICE_NAME_KEY = "ipc.invalidation.ticl.service_class";   + /** + * Name of the {@code <application>} metadata element whose value gives the Java class that + * implements the application's invalidation listener intent service. + */ + private static final String LISTENER_SERVICE_NAME_KEY = + "ipc.invalidation.ticl.listener_service_class"; +  /** Default values returned if not overriden by the manifest file. */  private static final Map<String, String> DEFAULTS = new HashMap<String, String>();  static {  DEFAULTS.put(TICL_SERVICE_NAME_KEY,  "com.google.ipc.invalidation.ticl.android2.TiclService");  DEFAULTS.put(LISTENER_NAME_KEY, ""); + DEFAULTS.put(LISTENER_SERVICE_NAME_KEY, + "com.google.ipc.invalidation.ticl.android2.AndroidInvalidationListenerStub");  }    private final Context context; @@ -69,6 +79,11 @@  return Preconditions.checkNotNull(readApplicationMetadata(LISTENER_NAME_KEY));  }   + /** Returns the name of the class implementing the invalidation listener intent service. */ + public String getListenerServiceClass() { + return Preconditions.checkNotNull(readApplicationMetadata(LISTENER_SERVICE_NAME_KEY)); + } +  /**  * Returns the metadata-provided value for {@code key} in {@code AndroidManifest.xml} if one  * exists, or the value from {@link #DEFAULTS} if one does not. @@ -79,8 +94,10 @@  // Read the manifest-provided value.  appInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(),  PackageManager.GET_META_DATA); - String value = appInfo.metaData.getString(key); - + String value = null; + if (appInfo.metaData != null) { + value = appInfo.metaData.getString(key); + }  // Return the manifest value if present or the default value if not.  return (value != null) ?  value : Preconditions.checkNotNull(DEFAULTS.get(key), "No default value for %s", key); 
diff --git a/src/java/com/google/ipc/invalidation/ticl/android2/ProtocolIntents.java b/src/java/com/google/ipc/invalidation/ticl/android2/ProtocolIntents.java index 7994401..c6852d8 100644 --- a/src/java/com/google/ipc/invalidation/ticl/android2/ProtocolIntents.java +++ b/src/java/com/google/ipc/invalidation/ticl/android2/ProtocolIntents.java 
@@ -54,23 +54,23 @@  static final Version ANDROID_PROTOCOL_VERSION_VALUE = CommonProtos2.newVersion(1, 0);    /** Key of Intent byte[] extra holding a client downcall protocol buffer. */ - static final String CLIENT_DOWNCALL_KEY = "ipcinv-downcall"; + public static final String CLIENT_DOWNCALL_KEY = "ipcinv-downcall";    /** Key of Intent byte[] extra holding an internal downcall protocol buffer. */ - static final String INTERNAL_DOWNCALL_KEY = "ipcinv-internal-downcall"; + public static final String INTERNAL_DOWNCALL_KEY = "ipcinv-internal-downcall";    /** Key of Intent byte[] extra holding a listener upcall protocol buffer. */ - static final String LISTENER_UPCALL_KEY = "ipcinv-upcall"; + public static final String LISTENER_UPCALL_KEY = "ipcinv-upcall";    /** Key of Intent byte[] extra holding a schedule event protocol buffer. */ - static final String SCHEDULER_KEY = "ipcinv-scheduler"; + public static final String SCHEDULER_KEY = "ipcinv-scheduler";    /** Key of Intent byte[] extra holding an outbound message protocol buffer. */  public static final String OUTBOUND_MESSAGE_KEY = "ipcinv-outbound-message";    /** Intents corresponding to calls on {@code InvalidationClient}. */ - static class ClientDowncalls { - static Intent newStartIntent() { + public static class ClientDowncalls { + public static Intent newStartIntent() {  Intent intent = new Intent();  intent.putExtra(CLIENT_DOWNCALL_KEY, newBuilder()  .setStart(StartDowncall.getDefaultInstance()) @@ -78,7 +78,7 @@  return intent;  }   - static Intent newStopIntent() { + public static Intent newStopIntent() {  Intent intent = new Intent();  intent.putExtra(CLIENT_DOWNCALL_KEY, newBuilder()  .setStop(StopDowncall.getDefaultInstance()) @@ -86,7 +86,7 @@  return intent;  }   - static Intent newAcknowledgeIntent(AckHandleP ackHandle) { + public static Intent newAcknowledgeIntent(AckHandleP ackHandle) {  AckDowncall ackDowncall = AckDowncall.newBuilder()  .setAckHandle(ackHandle.toByteString()).build();  Intent intent = new Intent(); @@ -95,7 +95,7 @@  return intent;  }   - static Intent newRegistrationIntent(Iterable<ObjectIdP> registrations) { + public static Intent newRegistrationIntent(Iterable<ObjectIdP> registrations) {  RegistrationDowncall regDowncall = RegistrationDowncall.newBuilder()  .addAllRegistrations(registrations).build();  Intent intent = new Intent(); @@ -104,7 +104,7 @@  return intent;  }   - static Intent newUnregistrationIntent(Iterable<ObjectIdP> unregistrations) { + public static Intent newUnregistrationIntent(Iterable<ObjectIdP> unregistrations) {  RegistrationDowncall unregDowncall = RegistrationDowncall.newBuilder()  .addAllUnregistrations(unregistrations).build();  Intent intent = new Intent(); @@ -116,6 +116,10 @@  private static ClientDowncall.Builder newBuilder() {  return ClientDowncall.newBuilder().setVersion(ANDROID_PROTOCOL_VERSION_VALUE);  } + + private ClientDowncalls() { + // Disallow instantiation. + }  }    /** Intents for non-public calls on the Ticl (currently, network-related calls. */ @@ -129,7 +133,7 @@  return intent;  }   - static Intent newNetworkStatusIntent(Boolean status) { + public static Intent newNetworkStatusIntent(Boolean status) {  Intent intent = new Intent();  intent.putExtra(INTERNAL_DOWNCALL_KEY,  newBuilder() @@ -162,18 +166,22 @@  private static InternalDowncall.Builder newBuilder() {  return InternalDowncall.newBuilder().setVersion(ANDROID_PROTOCOL_VERSION_VALUE);  } + + private InternalDowncalls() { + // Disallow instantiation. + }  }    /** Intents corresponding to calls on {@code InvalidationListener}. */ - static class ListenerUpcalls { - static Intent newReadyIntent() { + public static class ListenerUpcalls { + public static Intent newReadyIntent() {  Intent intent = new Intent();  intent.putExtra(LISTENER_UPCALL_KEY,  newBuilder().setReady(ReadyUpcall.getDefaultInstance()).build().toByteArray());  return intent;  }   - static Intent newInvalidateIntent(InvalidationP invalidation, AckHandleP ackHandle) { + public static Intent newInvalidateIntent(InvalidationP invalidation, AckHandleP ackHandle) {  Intent intent = new Intent();  InvalidateUpcall invUpcall = InvalidateUpcall.newBuilder()  .setAckHandle(ackHandle.toByteString()) @@ -183,7 +191,7 @@  return intent;  }   - static Intent newInvalidateUnknownIntent(ObjectIdP object, AckHandleP ackHandle) { + public static Intent newInvalidateUnknownIntent(ObjectIdP object, AckHandleP ackHandle) {  Intent intent = new Intent();  InvalidateUpcall invUpcall = InvalidateUpcall.newBuilder()  .setAckHandle(ackHandle.toByteString()) @@ -193,7 +201,7 @@  return intent;  }   - static Intent newInvalidateAllIntent(AckHandleP ackHandle) { + public static Intent newInvalidateAllIntent(AckHandleP ackHandle) {  Intent intent = new Intent();  InvalidateUpcall invUpcall = InvalidateUpcall.newBuilder()  .setAckHandle(ackHandle.toByteString()) @@ -203,7 +211,7 @@  return intent;  }   - static Intent newRegistrationStatusIntent(ObjectIdP object, boolean isRegistered) { + public static Intent newRegistrationStatusIntent(ObjectIdP object, boolean isRegistered) {  Intent intent = new Intent();  RegistrationStatusUpcall regUpcall = RegistrationStatusUpcall.newBuilder()  .setObjectId(object) @@ -213,7 +221,7 @@  return intent;  }   - static Intent newRegistrationFailureIntent(ObjectIdP object, boolean isTransient, + public static Intent newRegistrationFailureIntent(ObjectIdP object, boolean isTransient,  String message) {  Intent intent = new Intent();  RegistrationFailureUpcall regUpcall = RegistrationFailureUpcall.newBuilder() @@ -225,7 +233,7 @@  return intent;  }   - static Intent newReissueRegistrationsIntent(byte[] prefix, int length) { + public static Intent newReissueRegistrationsIntent(byte[] prefix, int length) {  Intent intent = new Intent();  ReissueRegistrationsUpcall reissueRegistrations = ReissueRegistrationsUpcall.newBuilder()  .setPrefix(ByteString.copyFrom(prefix)) @@ -235,7 +243,7 @@  return intent;  }   - static Intent newErrorIntent(ErrorInfo errorInfo) { + public static Intent newErrorIntent(ErrorInfo errorInfo) {  Intent intent = new Intent();  ErrorUpcall errorUpcall = ErrorUpcall.newBuilder()  .setErrorCode(errorInfo.getErrorReason()) @@ -250,10 +258,14 @@  private static ListenerUpcall.Builder newBuilder() {  return ListenerUpcall.newBuilder().setVersion(ANDROID_PROTOCOL_VERSION_VALUE);  } + + private ListenerUpcalls() { + // Disallow instantiation. + }  }    /** Returns a new intent encoding a request to execute the scheduled action {@code eventName}. */ - static Intent newSchedulerIntent(String eventName, long ticlId) { + public static Intent newSchedulerIntent(String eventName, long ticlId) {  byte[] eventBytes =  AndroidSchedulerEvent.newBuilder()  .setVersion(ANDROID_PROTOCOL_VERSION_VALUE) 
diff --git a/src/java/com/google/ipc/invalidation/ticl/android2/TiclService.java b/src/java/com/google/ipc/invalidation/ticl/android2/TiclService.java index 2f598aa..882513d 100644 --- a/src/java/com/google/ipc/invalidation/ticl/android2/TiclService.java +++ b/src/java/com/google/ipc/invalidation/ticl/android2/TiclService.java 
@@ -83,12 +83,14 @@  // TODO: We may want to use wakelocks to prevent the phone from sleeping  // before we have finished handling the Intent.  if (intent == null) { + resources.getLogger().fine("Ignoring null intent");  return;  }    // We create resources anew each time.  resources = createResources();  resources.start(); + resources.getLogger().fine("onHandleIntent(%s)", AndroidStrings.toLazyCompactString(intent));  validator = new AndroidIntentProtocolValidator(resources.getLogger());    // Dispatch the appropriate handler function based on which extra key is set. 
diff --git a/src/java/com/google/ipc/invalidation/util/Bytes.java b/src/java/com/google/ipc/invalidation/util/Bytes.java index 84bbd8c..d6c1507 100644 --- a/src/java/com/google/ipc/invalidation/util/Bytes.java +++ b/src/java/com/google/ipc/invalidation/util/Bytes.java 
@@ -119,6 +119,11 @@  return bytes;  }   + /** Converts this to a byte string. */ + public ByteString toByteString() { + return ByteString.copyFrom(getByteArray()); + } +  /**  * Returns a new {@code Bytes} containing the given subrange of bytes [{@code  * from}, {@code to}). 
diff --git a/src/java/com/google/ipc/invalidation/util/ExponentialBackoffDelayGenerator.java b/src/java/com/google/ipc/invalidation/util/ExponentialBackoffDelayGenerator.java index e8aaf93..75b8648 100644 --- a/src/java/com/google/ipc/invalidation/util/ExponentialBackoffDelayGenerator.java +++ b/src/java/com/google/ipc/invalidation/util/ExponentialBackoffDelayGenerator.java 
@@ -83,8 +83,8 @@  int delay = 0; // After a reset, the delay is 0.  if (inRetryMode) {   - // Generate the delay. - delay = (int) (random.nextDouble() * currentMaxDelay); + // Generate the delay in the range [1, currentMaxDelay]. + delay = random.nextInt(currentMaxDelay) + 1;    // Adjust the max for the next run.  int maxDelay = initialMaxDelay * maxExponentialFactor; 
diff --git a/src/java/com/google/ipc/invalidation/util/TextBuilder.java b/src/java/com/google/ipc/invalidation/util/TextBuilder.java index 6a1f358..7c7751f 100644 --- a/src/java/com/google/ipc/invalidation/util/TextBuilder.java +++ b/src/java/com/google/ipc/invalidation/util/TextBuilder.java 
@@ -16,6 +16,8 @@    package com.google.ipc.invalidation.util;   +import com.google.protobuf.ByteString; +  import java.lang.reflect.Field;    /** @@ -95,6 +97,12 @@  return this;  }   + /** Appends the {@link Bytes#toString} representation of {@code bytes} to this builder. */ + public TextBuilder append(ByteString bytes) { + builder.append(Bytes.toString(bytes)); + return this; + } +  /**  * Appends the string representation of {@code l} to this builder.  * 
diff --git a/src/proto/android_channel.proto b/src/proto/android_channel.proto index 52f6918..9309416 100644 --- a/src/proto/android_channel.proto +++ b/src/proto/android_channel.proto 
@@ -35,6 +35,8 @@  // channel version controls the expected envelope syntax and semantics of  // http and c2dm messages sent between the client and server.  enum MajorVersion { +  +  // The initial version of the android channel protocol. Inbound and  // outbound channel packets contained a single binary protocol message only.  INITIAL = 0; 
diff --git a/src/proto/android_listener.proto b/src/proto/android_listener.proto new file mode 100644 index 0000000..1755288 --- /dev/null +++ b/src/proto/android_listener.proto 
@@ -0,0 +1,101 @@ +/* + * Copyright 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// +// Specification of protocols used by the AndroidListener abstraction. +// +// Note: unless otherwise specified in a comment, all fields in all messages +// are required, even though they are listed as optional. + +syntax = "proto2"; + +package com.google.protos.ipc.invalidation; + +option optimize_for = LITE_RUNTIME; + +option java_outer_classname = "AndroidListenerProtocol"; + + + + + +import "client.proto"; +import "client_protocol.proto"; + +// Used to persist internal state between instantiations of Android listener +// objects. +message AndroidListenerState { + // When a registration request has failed, we track state for that object that + // allows retries to be delayed using exponential backoff. + message RetryRegistrationState { + // Identifier of the object for which there has been a failure. + optional ObjectIdP object_id = 1; + + // State of exponential backoff delay generator that is used to delay any + // registration retries for the object. + optional ExponentialBackoffState exponential_backoff_state = 2; + } + + // Set of object ids tracking the application's desired registrations. + repeated ObjectIdP registration = 1; + + // Set of states for registrations retries. When there is a transient + // registration failure relative to an object, an entry is added. If + // registration is successful or the user gives up on the request, the entry + // is removed. + repeated RetryRegistrationState retry_registration_state = 2; + + // Identifier of client with which this listener is associated. This client ID + // is randomly generated by the Android listener whenever a new client is + // started and has no relationship to 's application client ID. + optional bytes client_id = 3; + + // Sequence number for alarm manager request codes. Sequence numbers are + // assigned serially for each distinct client_id. This value indicates + // the request code used for the last request. + optional int32 request_code_seq_num = 4; +} + +// Represents a command that registers or unregisters a set of objects. The +// command may be initiated by the application or by the Android listener when +// there is a registration failure. +message RegistrationCommand { + // Indicates whether this is a register command (when true) or unregister + // (when false) request. + optional bool is_register = 1; + + // Identifies the objects to register or unregister. + repeated ObjectIdP object_id = 2; + + // Identifier of client with which this listener is associated. + optional bytes client_id = 3; + + // Indicates whether this is a delayed registration command. When a + // registration command intent is handled by the Android listener, this field + // is used to determine whether the command has been delayed yet or not. If it + // has not already been delayed, the listener may choose to defer the command + // until later. + optional bool is_delayed = 4; +} + +// Represents a command that starts an Android invalidation client. +message StartCommand { + // Type of client to start. + optional int32 client_type = 1; + + // Name of client to start. + optional bytes client_name = 2; +} + 
diff --git a/src/proto/android_state.proto b/src/proto/android_state.proto index 55d920c..fc444e2 100644 --- a/src/proto/android_state.proto +++ b/src/proto/android_state.proto 
@@ -27,7 +27,6 @@       -// TODO: Factor out Version and remove this dependency  import "client_protocol.proto";    // Base metadata for an Android client instance. All of these values 
diff --git a/src/proto/channel.proto b/src/proto/channel.proto index 068c78a..807c0a4 100644 --- a/src/proto/channel.proto +++ b/src/proto/channel.proto 
@@ -47,8 +47,6 @@    // Message sent from the client to the server and vice-versa via the  // delivery service. -// TODO: rename AddressedMessage to -// RegistrarGatewayMessage in a separate CL.  message AddressedMessage {  // The encoding type for the network_message field.  optional ChannelMessageEncoding.MessageEncoding encoding = 1; @@ -69,15 +67,3 @@  // Response to AddressedMessage  message AddressedMessageResponse {  } - -// Message batched from the client to the server and vice-versa via the -// delivery service. -message AddressedMessageBatch { - repeated AddressedMessage requests = 1; -} - -// Responses to AddressedMessageBatch, containing batched responses to -// each of AddressedMessage -message AddressedMessageBatchResponse { - repeated AddressedMessageResponse responses = 1; -} 
diff --git a/src/proto/java_client.proto b/src/proto/java_client.proto index 77b39ec..7e8fcf0 100644 --- a/src/proto/java_client.proto +++ b/src/proto/java_client.proto 
@@ -56,6 +56,7 @@  message RegistrationManagerStateP {  repeated ObjectIdP registrations = 1;  optional RegistrationSummary last_known_server_summary = 2; + repeated RegistrationP pending_operations = 3;  }    // State of a recurring task. Fields correspond directly to fields in